Skip to content

Commit 14b8092

Browse files
committed
time out fetch
1 parent 654d5f7 commit 14b8092

File tree

5 files changed

+214
-33
lines changed

5 files changed

+214
-33
lines changed

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

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
getBodySize,
1818
getBodyString,
1919
makeNetworkReplayBreadcrumb,
20-
mergeWarningsIntoMeta,
20+
mergeWarning,
2121
parseContentLengthHeader,
2222
urlMatches,
2323
} from './networkUtils';
@@ -123,20 +123,21 @@ function _getRequestInfo(
123123
const [bodyStr, warning] = getBodyString(requestBody);
124124
const data = buildNetworkRequestOrResponse(headers, requestBodySize, bodyStr);
125125

126-
if (data && warning) {
127-
data._meta = mergeWarningsIntoMeta(data._meta, [warning]);
126+
if (warning) {
127+
return mergeWarning(data, warning);
128128
}
129129

130130
return data;
131131
}
132132

133-
async function _getResponseInfo(
133+
/** Exported only for tests. */
134+
export async function _getResponseInfo(
134135
captureDetails: boolean,
135136
{
136137
networkCaptureBodies,
137138
textEncoder,
138139
networkResponseHeaders,
139-
}: ReplayNetworkOptions & {
140+
}: Pick<ReplayNetworkOptions, 'networkCaptureBodies' | 'networkResponseHeaders'> & {
140141
textEncoder: TextEncoderInternal;
141142
},
142143
response: Response | undefined,
@@ -152,8 +153,6 @@ async function _getResponseInfo(
152153
return buildNetworkRequestOrResponse(headers, responseBodySize, undefined);
153154
}
154155

155-
// Only clone the response if we need to
156-
157156
const [bodyText, warning] = await _parseFetchResponseBody(response);
158157
const result = getResponseData(bodyText, {
159158
networkCaptureBodies,
@@ -163,8 +162,8 @@ async function _getResponseInfo(
163162
headers,
164163
});
165164

166-
if (result && warning) {
167-
result._meta = mergeWarningsIntoMeta(result._meta, [warning]);
165+
if (warning) {
166+
return mergeWarning(result, warning);
168167
}
169168

170169
return result;
@@ -209,13 +208,17 @@ function getResponseData(
209208
}
210209

211210
async function _parseFetchResponseBody(response: Response): Promise<[string | undefined, NetworkMetaWarning?]> {
211+
const res = _tryCloneResponse(response);
212+
213+
if (!res) {
214+
return [undefined, 'BODY_PARSE_ERROR'];
215+
}
216+
212217
try {
213-
// We have to clone this, as the body can only be read once
214-
const res = response.clone();
215-
const text = await res.text();
218+
const text = await _tryGetResponseText(res);
216219
return [text];
217-
} catch (errorr) {
218-
__DEBUG_BUILD__ && logger.warn('[Replay] Failed to read response body', errorr);
220+
} catch (error) {
221+
__DEBUG_BUILD__ && logger.warn('[Replay] Failed to get text body from response', error);
219222
return [undefined, 'BODY_PARSE_ERROR'];
220223
}
221224
}
@@ -278,3 +281,39 @@ function getHeadersFromOptions(
278281

279282
return getAllowedHeaders(headers, allowedHeaders);
280283
}
284+
285+
function _tryCloneResponse(response: Response): Response | void {
286+
try {
287+
// We have to clone this, as the body can only be read once
288+
return response.clone();
289+
} catch (error) {
290+
// this can throw if the response was already consumed before
291+
__DEBUG_BUILD__ && logger.warn('[Replay] Failed to clone response body', error);
292+
}
293+
}
294+
295+
/**
296+
* Get the response body of a fetch request, or timeout after 500ms.
297+
* Fetch can return a streaming body, that may not resolve (or not for a long time).
298+
* If that happens, we rather abort after a short time than keep waiting for this.
299+
*/
300+
function _tryGetResponseText(response: Response): Promise<string | undefined> {
301+
return new Promise((resolve, reject) => {
302+
const timeout = setTimeout(() => reject(new Error('Timeout while trying to read response body')), 500);
303+
304+
_getResponseText(response)
305+
.then(
306+
txt => resolve(txt),
307+
reason => reject(reason),
308+
)
309+
.finally(() => clearTimeout(timeout));
310+
});
311+
312+
return _getResponseText(response);
313+
}
314+
315+
async function _getResponseText(response: Response): Promise<string> {
316+
// Force this to be a promise, just to be safe
317+
// eslint-disable-next-line no-return-await
318+
return await response.text();
319+
}

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

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,27 @@ export function getBodyString(body: unknown): [string | undefined, NetworkMetaWa
8484
return [undefined];
8585
}
8686

87-
/** Merge warnings into an possibly existing meta. */
88-
export function mergeWarningsIntoMeta(
89-
meta: ReplayNetworkRequestOrResponse['_meta'],
90-
warnings: NetworkMetaWarning[],
91-
): ReplayNetworkRequestOrResponse['_meta'] {
92-
const newMeta = { ...meta };
87+
/** Merge a warning into an existing network request/response. */
88+
export function mergeWarning(
89+
info: ReplayNetworkRequestOrResponse | undefined,
90+
warning: NetworkMetaWarning,
91+
): ReplayNetworkRequestOrResponse {
92+
if (!info) {
93+
return {
94+
headers: {},
95+
size: undefined,
96+
_meta: {
97+
warnings: [warning],
98+
},
99+
};
100+
}
101+
102+
const newMeta = { ...info._meta };
93103
const existingWarnings = newMeta.warnings || [];
94-
newMeta.warnings = [...existingWarnings, ...warnings];
104+
newMeta.warnings = [...existingWarnings, warning];
95105

96-
return newMeta;
106+
info._meta = newMeta;
107+
return info;
97108
}
98109

99110
/** Convert ReplayNetworkRequestData to a PerformanceEntry. */

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

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
getBodySize,
1717
getBodyString,
1818
makeNetworkReplayBreadcrumb,
19-
mergeWarningsIntoMeta,
19+
mergeWarning,
2020
parseContentLengthHeader,
2121
urlMatches,
2222
} from './networkUtils';
@@ -116,22 +116,14 @@ function _prepareXhrData(
116116
const request = buildNetworkRequestOrResponse(networkRequestHeaders, requestBodySize, requestBody);
117117
const response = buildNetworkRequestOrResponse(networkResponseHeaders, responseBodySize, responseBody);
118118

119-
if (request && requestWarning) {
120-
request._meta = mergeWarningsIntoMeta(request._meta, [requestWarning]);
121-
}
122-
123-
if (response && responseWarning) {
124-
response._meta = mergeWarningsIntoMeta(response._meta, [responseWarning]);
125-
}
126-
127119
return {
128120
startTimestamp,
129121
endTimestamp,
130122
url,
131123
method,
132124
statusCode,
133-
request,
134-
response,
125+
request: requestWarning ? mergeWarning(request, requestWarning) : request,
126+
response: responseWarning ? mergeWarning(response, responseWarning) : response,
135127
};
136128
}
137129

packages/replay/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ async function waitForReplayEventBuffer() {
2323
await Promise.resolve();
2424
await Promise.resolve();
2525
await Promise.resolve();
26+
await Promise.resolve();
27+
await Promise.resolve();
2628
}
2729

2830
const LARGE_BODY = 'a'.repeat(NETWORK_BODY_MAX_SIZE + 1);
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { TextEncoder } from 'util';
2+
3+
import { _getResponseInfo } from '../../../../src/coreHandlers/util/fetchUtils';
4+
5+
describe('_getResponseInfo', () => {
6+
it('works with captureDetails: false', async () => {
7+
const res = await _getResponseInfo(
8+
false,
9+
{
10+
networkCaptureBodies: true,
11+
textEncoder: new TextEncoder(),
12+
networkResponseHeaders: [],
13+
},
14+
undefined,
15+
undefined,
16+
);
17+
18+
expect(res).toEqual(undefined);
19+
});
20+
21+
it('works with captureDetails: false & responseBodySize', async () => {
22+
const res = await _getResponseInfo(
23+
false,
24+
{
25+
networkCaptureBodies: true,
26+
textEncoder: new TextEncoder(),
27+
networkResponseHeaders: [],
28+
},
29+
undefined,
30+
123,
31+
);
32+
33+
expect(res).toEqual({
34+
headers: {},
35+
size: 123,
36+
_meta: {
37+
warnings: ['URL_SKIPPED'],
38+
},
39+
});
40+
});
41+
42+
it('works with text body', async () => {
43+
const response = {
44+
headers: {
45+
has: () => {
46+
return false;
47+
},
48+
get: () => {
49+
return undefined;
50+
},
51+
},
52+
clone: () => response,
53+
text: () => Promise.resolve('text body'),
54+
} as unknown as Response;
55+
56+
const res = await _getResponseInfo(
57+
true,
58+
{
59+
networkCaptureBodies: true,
60+
textEncoder: new TextEncoder(),
61+
networkResponseHeaders: [],
62+
},
63+
response,
64+
undefined,
65+
);
66+
67+
expect(res).toEqual({
68+
headers: {},
69+
size: 9,
70+
body: 'text body',
71+
});
72+
});
73+
74+
it('works with body that fails', async () => {
75+
const response = {
76+
headers: {
77+
has: () => {
78+
return false;
79+
},
80+
get: () => {
81+
return undefined;
82+
},
83+
},
84+
clone: () => response,
85+
text: () => Promise.reject('cannot read'),
86+
} as unknown as Response;
87+
88+
const res = await _getResponseInfo(
89+
true,
90+
{
91+
networkCaptureBodies: true,
92+
textEncoder: new TextEncoder(),
93+
networkResponseHeaders: [],
94+
},
95+
response,
96+
undefined,
97+
);
98+
99+
expect(res).toEqual({
100+
_meta: { warnings: ['BODY_PARSE_ERROR'] },
101+
headers: {},
102+
size: undefined,
103+
});
104+
});
105+
106+
it('works with body that times out', async () => {
107+
const response = {
108+
headers: {
109+
has: () => {
110+
return false;
111+
},
112+
get: () => {
113+
return undefined;
114+
},
115+
},
116+
clone: () => response,
117+
text: () => new Promise(resolve => setTimeout(() => resolve('text body'), 1000)),
118+
} as unknown as Response;
119+
120+
const res = await _getResponseInfo(
121+
true,
122+
{
123+
networkCaptureBodies: true,
124+
textEncoder: new TextEncoder(),
125+
networkResponseHeaders: [],
126+
},
127+
response,
128+
undefined,
129+
);
130+
131+
expect(res).toEqual({
132+
_meta: { warnings: ['BODY_PARSE_ERROR'] },
133+
headers: {},
134+
size: undefined,
135+
});
136+
});
137+
});

0 commit comments

Comments
 (0)