@sentry/react-native
Version:
Official Sentry SDK for react-native
200 lines • 6.7 kB
JavaScript
import { encodeUTF8 } from '../utils/encode';
/** Convert a Content-Length header to number/undefined. */
export function parseContentLengthHeader(header) {
if (!header) {
return undefined;
}
const size = parseInt(header, 10);
return isNaN(size) ? undefined : size;
}
/** Get the size of a body. */
export function getBodySize(body) {
if (!body) {
return undefined;
}
try {
if (typeof body === 'string') {
return encodeUTF8(body).length;
}
if (body instanceof URLSearchParams) {
return encodeUTF8(body.toString()).length;
}
if (body instanceof FormData) {
const formDataStr = _serializeFormData(body);
return encodeUTF8(formDataStr).length;
}
if (body instanceof Blob) {
return body.size;
}
if (body instanceof ArrayBuffer) {
return body.byteLength;
}
// Currently unhandled types: ArrayBufferView, ReadableStream
}
catch (_a) {
// just return undefined
}
return undefined;
}
function _serializeFormData(formData) {
// This is a bit simplified, but gives us a decent estimate
// This converts e.g. { name: 'Anne Smith', age: 13 } to 'name=Anne+Smith&age=13'
// @ts-expect-error passing FormData to URLSearchParams won't correctly serialize `File` entries, which is fine for this use-case. See https://github.com/microsoft/TypeScript/issues/30584
return new URLSearchParams(formData).toString();
}
export const NETWORK_BODY_MAX_SIZE = 150000;
export const DEFAULT_NETWORK_HEADERS = ['content-type', 'content-length', 'accept'];
const DENY_HEADERS = new Set([
'authorization',
'cookie',
'set-cookie',
'x-api-key',
'x-auth-token',
'proxy-authorization',
]);
/**
* Check if a URL matches any pattern in the list. Strings use substring match
* (empty strings are ignored to avoid matching everything by accident);
* RegExp uses `.test()` with `lastIndex` reset so global-flag patterns are
* stateless across calls.
*/
function _urlMatches(url, patterns) {
for (const pattern of patterns) {
if (typeof pattern === 'string') {
if (pattern.length > 0 && url.indexOf(pattern) !== -1) {
return true;
}
}
else if (pattern instanceof RegExp) {
// Reset lastIndex to make /g and /y patterns idempotent across calls.
pattern.lastIndex = 0;
if (pattern.test(url)) {
return true;
}
}
}
return false;
}
/**
* Whether to capture full network details (headers, bodies) for a given URL,
* based on the allow/deny URL lists.
*/
export function shouldCaptureNetworkDetails(url, options) {
if (!url || options.allowUrls.length === 0) {
return false;
}
if (!_urlMatches(url, options.allowUrls)) {
return false;
}
if (options.denyUrls.length > 0 && _urlMatches(url, options.denyUrls)) {
return false;
}
return true;
}
/**
* Serialize a request/response body to a string, truncated to NETWORK_BODY_MAX_SIZE.
* Returns undefined if the body is empty/missing; returns a meta warning if unserializable
* or truncated.
*/
export function getBodyString(body) {
if (body == null) {
return undefined;
}
try {
let bodyStr;
if (typeof body === 'string') {
bodyStr = body;
}
else if (typeof body === 'number' || typeof body === 'boolean') {
// JSON primitives (e.g. `xhr.response` with responseType='json' returning `42` or `true`)
bodyStr = String(body);
}
else if (body instanceof URLSearchParams) {
bodyStr = body.toString();
}
else if (body instanceof FormData) {
bodyStr = _serializeFormData(body);
}
else if (body instanceof Blob || body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
// Binary payloads (Blob, ArrayBuffer, typed arrays like Uint8Array)
// can't be safely inlined as text; record the type but skip the body.
return { _meta: { warnings: ['UNPARSEABLE_BODY_TYPE'] } };
}
else if (typeof body === 'object') {
// Last-ditch attempt: try to JSON-stringify plain objects (e.g. xhr.response with responseType='json')
try {
bodyStr = JSON.stringify(body);
}
catch (_a) {
return { _meta: { warnings: ['UNPARSEABLE_BODY_TYPE'] } };
}
}
if (bodyStr === undefined) {
return { _meta: { warnings: ['UNPARSEABLE_BODY_TYPE'] } };
}
if (bodyStr.length > NETWORK_BODY_MAX_SIZE) {
return {
body: bodyStr.slice(0, NETWORK_BODY_MAX_SIZE),
_meta: { warnings: ['MAX_BODY_SIZE_EXCEEDED'] },
};
}
return { body: bodyStr };
}
catch (_b) {
return { _meta: { warnings: ['UNPARSEABLE_BODY_TYPE'] } };
}
}
/**
* Filter a headers map down to the set explicitly captured (defaults + user-supplied)
* and strip authorization-like headers. Header name comparison is case-insensitive;
* the returned keys are lowercased.
*/
export function filterHeaders(headers, extraAllowed) {
if (!headers) {
return undefined;
}
const allowed = new Set([...DEFAULT_NETWORK_HEADERS, ...extraAllowed.map(h => h.toLowerCase())]);
const out = {};
let count = 0;
for (const rawName of Object.keys(headers)) {
const name = rawName.toLowerCase();
if (DENY_HEADERS.has(name)) {
continue;
}
if (!allowed.has(name)) {
continue;
}
const value = headers[rawName];
if (value == null) {
continue;
}
out[name] = value;
count += 1;
}
return count > 0 ? out : undefined;
}
/** Parse the raw string returned by XMLHttpRequest.getAllResponseHeaders() into a record. */
export function parseAllResponseHeaders(raw) {
if (!raw) {
return {};
}
const result = {};
// Headers are CRLF-delimited; some implementations use LF only.
const lines = raw.split(/\r?\n/);
for (const line of lines) {
if (!line) {
continue;
}
const idx = line.indexOf(':');
if (idx === -1) {
continue;
}
const name = line.slice(0, idx).trim();
const value = line.slice(idx + 1).trim();
if (name) {
result[name] = value;
}
}
return result;
}
//# sourceMappingURL=networkUtils.js.map