@@ -3,6 +3,7 @@ import { logger } from '@sentry/utils';
33
44import type {
55 FetchHint ,
6+ NetworkMetaWarning ,
67 ReplayContainer ,
78 ReplayNetworkOptions ,
89 ReplayNetworkRequestData ,
@@ -16,6 +17,7 @@ import {
1617 getBodySize ,
1718 getBodyString ,
1819 makeNetworkReplayBreadcrumb ,
20+ mergeWarning ,
1921 parseContentLengthHeader ,
2022 urlMatches ,
2123} from './networkUtils' ;
@@ -118,17 +120,24 @@ function _getRequestInfo(
118120
119121 // We only want to transmit string or string-like bodies
120122 const requestBody = _getFetchRequestArgBody ( input ) ;
121- const bodyStr = getBodyString ( requestBody ) ;
122- return buildNetworkRequestOrResponse ( headers , requestBodySize , bodyStr ) ;
123+ const [ bodyStr , warning ] = getBodyString ( requestBody ) ;
124+ const data = buildNetworkRequestOrResponse ( headers , requestBodySize , bodyStr ) ;
125+
126+ if ( warning ) {
127+ return mergeWarning ( data , warning ) ;
128+ }
129+
130+ return data ;
123131}
124132
125- async function _getResponseInfo (
133+ /** Exported only for tests. */
134+ export async function _getResponseInfo (
126135 captureDetails : boolean ,
127136 {
128137 networkCaptureBodies,
129138 textEncoder,
130139 networkResponseHeaders,
131- } : ReplayNetworkOptions & {
140+ } : Pick < ReplayNetworkOptions , 'networkCaptureBodies' | 'networkResponseHeaders' > & {
132141 textEncoder : TextEncoderInternal ;
133142 } ,
134143 response : Response | undefined ,
@@ -144,12 +153,39 @@ async function _getResponseInfo(
144153 return buildNetworkRequestOrResponse ( headers , responseBodySize , undefined ) ;
145154 }
146155
147- // Only clone the response if we need to
148- try {
149- // We have to clone this, as the body can only be read once
150- const res = response . clone ( ) ;
151- const bodyText = await _parseFetchBody ( res ) ;
156+ const [ bodyText , warning ] = await _parseFetchResponseBody ( response ) ;
157+ const result = getResponseData ( bodyText , {
158+ networkCaptureBodies,
159+ textEncoder,
160+ responseBodySize,
161+ captureDetails,
162+ headers,
163+ } ) ;
164+
165+ if ( warning ) {
166+ return mergeWarning ( result , warning ) ;
167+ }
152168
169+ return result ;
170+ }
171+
172+ function getResponseData (
173+ bodyText : string | undefined ,
174+ {
175+ networkCaptureBodies,
176+ textEncoder,
177+ responseBodySize,
178+ captureDetails,
179+ headers,
180+ } : {
181+ captureDetails : boolean ;
182+ networkCaptureBodies : boolean ;
183+ responseBodySize : number | undefined ;
184+ headers : Record < string , string > ;
185+ textEncoder : TextEncoderInternal ;
186+ } ,
187+ ) : ReplayNetworkRequestOrResponse | undefined {
188+ try {
153189 const size =
154190 bodyText && bodyText . length && responseBodySize === undefined
155191 ? getBodySize ( bodyText , textEncoder )
@@ -171,11 +207,19 @@ async function _getResponseInfo(
171207 }
172208}
173209
174- async function _parseFetchBody ( response : Response ) : Promise < string | undefined > {
210+ 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+
175217 try {
176- return await response . text ( ) ;
177- } catch {
178- return undefined ;
218+ const text = await _tryGetResponseText ( res ) ;
219+ return [ text ] ;
220+ } catch ( error ) {
221+ __DEBUG_BUILD__ && logger . warn ( '[Replay] Failed to get text body from response' , error ) ;
222+ return [ undefined , 'BODY_PARSE_ERROR' ] ;
179223 }
180224}
181225
@@ -237,3 +281,39 @@ function getHeadersFromOptions(
237281
238282 return getAllowedHeaders ( headers , allowedHeaders ) ;
239283}
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+ }
0 commit comments