Skip to content

Commit 04e7be9

Browse files
authored
fix(replay): Add additional safeguards for capturing network bodies (#9506)
This adds additional safeguards around fetch/xhr body capturing for replay. I added additional try-catch in all places that depend on `networkCaptureBodies`. This also types the fetch/xhr hints as `Partial` to ensure we guard against any of the things not actually being defined, to be on the safe side. Hopefully fixes #9339
1 parent 1d50eef commit 04e7be9

File tree

3 files changed

+55
-36
lines changed

3 files changed

+55
-36
lines changed

packages/replay/src/coreHandlers/util/fetchUtils.ts

+13-11
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
*/
2727
export async function captureFetchBreadcrumbToReplay(
2828
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
29-
hint: FetchHint,
29+
hint: Partial<FetchHint>,
3030
options: ReplayNetworkOptions & {
3131
textEncoder: TextEncoderInternal;
3232
replay: ReplayContainer;
@@ -50,12 +50,12 @@ export async function captureFetchBreadcrumbToReplay(
5050
*/
5151
export function enrichFetchBreadcrumb(
5252
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
53-
hint: FetchHint,
53+
hint: Partial<FetchHint>,
5454
options: { textEncoder: TextEncoderInternal },
5555
): void {
5656
const { input, response } = hint;
5757

58-
const body = _getFetchRequestArgBody(input);
58+
const body = input ? _getFetchRequestArgBody(input) : undefined;
5959
const reqSize = getBodySize(body, options.textEncoder);
6060

6161
const resSize = response ? parseContentLengthHeader(response.headers.get('content-length')) : undefined;
@@ -70,12 +70,13 @@ export function enrichFetchBreadcrumb(
7070

7171
async function _prepareFetchData(
7272
breadcrumb: Breadcrumb & { data: FetchBreadcrumbData },
73-
hint: FetchHint,
73+
hint: Partial<FetchHint>,
7474
options: ReplayNetworkOptions & {
7575
textEncoder: TextEncoderInternal;
7676
},
7777
): Promise<ReplayNetworkRequestData> {
78-
const { startTimestamp, endTimestamp } = hint;
78+
const now = Date.now();
79+
const { startTimestamp = now, endTimestamp = now } = hint;
7980

8081
const {
8182
url,
@@ -106,10 +107,10 @@ async function _prepareFetchData(
106107

107108
function _getRequestInfo(
108109
{ networkCaptureBodies, networkRequestHeaders }: ReplayNetworkOptions,
109-
input: FetchHint['input'],
110+
input: FetchHint['input'] | undefined,
110111
requestBodySize?: number,
111112
): ReplayNetworkRequestOrResponse | undefined {
112-
const headers = getRequestHeaders(input, networkRequestHeaders);
113+
const headers = input ? getRequestHeaders(input, networkRequestHeaders) : {};
113114

114115
if (!networkCaptureBodies) {
115116
return buildNetworkRequestOrResponse(headers, requestBodySize, undefined);
@@ -130,16 +131,16 @@ async function _getResponseInfo(
130131
}: ReplayNetworkOptions & {
131132
textEncoder: TextEncoderInternal;
132133
},
133-
response: Response,
134+
response: Response | undefined,
134135
responseBodySize?: number,
135136
): Promise<ReplayNetworkRequestOrResponse | undefined> {
136137
if (!captureDetails && responseBodySize !== undefined) {
137138
return buildSkippedNetworkRequestOrResponse(responseBodySize);
138139
}
139140

140-
const headers = getAllHeaders(response.headers, networkResponseHeaders);
141+
const headers = response ? getAllHeaders(response.headers, networkResponseHeaders) : {};
141142

142-
if (!networkCaptureBodies && responseBodySize !== undefined) {
143+
if (!response || (!networkCaptureBodies && responseBodySize !== undefined)) {
143144
return buildNetworkRequestOrResponse(headers, responseBodySize, undefined);
144145
}
145146

@@ -163,7 +164,8 @@ async function _getResponseInfo(
163164
}
164165

165166
return buildNetworkRequestOrResponse(headers, size, undefined);
166-
} catch {
167+
} catch (error) {
168+
__DEBUG_BUILD__ && logger.warn('[Replay] Failed to serialize response body', error);
167169
// fallback
168170
return buildNetworkRequestOrResponse(headers, responseBodySize, undefined);
169171
}

packages/replay/src/coreHandlers/util/networkUtils.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { TextEncoderInternal } from '@sentry/types';
2-
import { dropUndefinedKeys, stringMatchesSomePattern } from '@sentry/utils';
2+
import { dropUndefinedKeys, logger, stringMatchesSomePattern } from '@sentry/utils';
33

44
import { NETWORK_BODY_MAX_SIZE, WINDOW } from '../../constants';
55
import type {
@@ -62,16 +62,20 @@ export function parseContentLengthHeader(header: string | null | undefined): num
6262

6363
/** Get the string representation of a body. */
6464
export function getBodyString(body: unknown): string | undefined {
65-
if (typeof body === 'string') {
66-
return body;
67-
}
65+
try {
66+
if (typeof body === 'string') {
67+
return body;
68+
}
6869

69-
if (body instanceof URLSearchParams) {
70-
return body.toString();
71-
}
70+
if (body instanceof URLSearchParams) {
71+
return body.toString();
72+
}
7273

73-
if (body instanceof FormData) {
74-
return _serializeFormData(body);
74+
if (body instanceof FormData) {
75+
return _serializeFormData(body);
76+
}
77+
} catch {
78+
__DEBUG_BUILD__ && logger.warn('[Replay] Failed to serialize body', body);
7579
}
7680

7781
return undefined;
@@ -199,7 +203,6 @@ function normalizeNetworkBody(body: string | undefined): {
199203
}
200204

201205
const exceedsSizeLimit = body.length > NETWORK_BODY_MAX_SIZE;
202-
203206
const isProbablyJson = _strIsProbablyJson(body);
204207

205208
if (exceedsSizeLimit) {

packages/replay/src/coreHandlers/util/xhrUtils.ts

+29-15
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
*/
2121
export async function captureXhrBreadcrumbToReplay(
2222
breadcrumb: Breadcrumb & { data: XhrBreadcrumbData },
23-
hint: XhrHint,
23+
hint: Partial<XhrHint>,
2424
options: ReplayNetworkOptions & { replay: ReplayContainer },
2525
): Promise<void> {
2626
try {
@@ -41,11 +41,15 @@ export async function captureXhrBreadcrumbToReplay(
4141
*/
4242
export function enrichXhrBreadcrumb(
4343
breadcrumb: Breadcrumb & { data: XhrBreadcrumbData },
44-
hint: XhrHint,
44+
hint: Partial<XhrHint>,
4545
options: { textEncoder: TextEncoderInternal },
4646
): void {
4747
const { xhr, input } = hint;
4848

49+
if (!xhr) {
50+
return;
51+
}
52+
4953
const reqSize = getBodySize(input, options.textEncoder);
5054
const resSize = xhr.getResponseHeader('content-length')
5155
? parseContentLengthHeader(xhr.getResponseHeader('content-length'))
@@ -61,10 +65,11 @@ export function enrichXhrBreadcrumb(
6165

6266
function _prepareXhrData(
6367
breadcrumb: Breadcrumb & { data: XhrBreadcrumbData },
64-
hint: XhrHint,
68+
hint: Partial<XhrHint>,
6569
options: ReplayNetworkOptions,
6670
): ReplayNetworkRequestData | null {
67-
const { startTimestamp, endTimestamp, input, xhr } = hint;
71+
const now = Date.now();
72+
const { startTimestamp = now, endTimestamp = now, input, xhr } = hint;
6873

6974
const {
7075
url,
@@ -78,7 +83,7 @@ function _prepareXhrData(
7883
return null;
7984
}
8085

81-
if (!urlMatches(url, options.networkDetailAllowUrls) || urlMatches(url, options.networkDetailDenyUrls)) {
86+
if (!xhr || !urlMatches(url, options.networkDetailAllowUrls) || urlMatches(url, options.networkDetailDenyUrls)) {
8287
const request = buildSkippedNetworkRequestOrResponse(requestBodySize);
8388
const response = buildSkippedNetworkRequestOrResponse(responseBodySize);
8489
return {
@@ -98,16 +103,11 @@ function _prepareXhrData(
98103
: {};
99104
const networkResponseHeaders = getAllowedHeaders(getResponseHeaders(xhr), options.networkResponseHeaders);
100105

101-
const request = buildNetworkRequestOrResponse(
102-
networkRequestHeaders,
103-
requestBodySize,
104-
options.networkCaptureBodies ? getBodyString(input) : undefined,
105-
);
106-
const response = buildNetworkRequestOrResponse(
107-
networkResponseHeaders,
108-
responseBodySize,
109-
options.networkCaptureBodies ? hint.xhr.responseText : undefined,
110-
);
106+
const requestBody = options.networkCaptureBodies ? getBodyString(input) : undefined;
107+
const responseBody = options.networkCaptureBodies ? _getXhrResponseBody(xhr) : undefined;
108+
109+
const request = buildNetworkRequestOrResponse(networkRequestHeaders, requestBodySize, requestBody);
110+
const response = buildNetworkRequestOrResponse(networkResponseHeaders, responseBodySize, responseBody);
111111

112112
return {
113113
startTimestamp,
@@ -133,3 +133,17 @@ function getResponseHeaders(xhr: XMLHttpRequest): Record<string, string> {
133133
return acc;
134134
}, {});
135135
}
136+
137+
function _getXhrResponseBody(xhr: XMLHttpRequest): string | undefined {
138+
try {
139+
return xhr.responseText;
140+
} catch {} // eslint-disable-line no-empty
141+
142+
// Try to manually parse the response body, if responseText fails
143+
try {
144+
const response = xhr.response;
145+
return getBodyString(response);
146+
} catch {} // eslint-disable-line no-empty
147+
148+
return undefined;
149+
}

0 commit comments

Comments
 (0)