@@ -67,6 +67,12 @@ const { bytesMatch } = require('../subresource-integrity/subresource-integrity')
6767const { isomorphicEncode } = require ( '../infra' )
6868
6969const GET_OR_HEAD = [ 'GET' , 'HEAD' ]
70+ const retriableFetchNetworkErrorCodes = new Set ( [
71+ 'ECONNRESET' ,
72+ 'EPIPE' ,
73+ 'UND_ERR_SOCKET'
74+ ] )
75+ const kFetchNetworkErrorRetry = Symbol ( 'fetch network error retry' )
7076
7177const defaultUserAgent = typeof __UNDICI_IS_NODE__ !== 'undefined' || typeof esbuildDetection !== 'undefined'
7278 ? 'node'
@@ -151,6 +157,34 @@ class Fetch extends EE {
151157 }
152158}
153159
160+ function createFetchConnection ( fetchParams ) {
161+ fetchParams . controller . connection = {
162+ abort : null ,
163+ destroyed : false ,
164+ destroy ( err , abort = true ) {
165+ if ( ! this . destroyed ) {
166+ this . destroyed = true
167+ if ( abort ) {
168+ this . abort ?. ( err ?? new DOMException ( 'The operation was aborted.' , 'AbortError' ) )
169+ }
170+ }
171+ }
172+ }
173+ }
174+
175+ function canReplayRequestBody ( request ) {
176+ return request . body == null || request . body . source != null
177+ }
178+
179+ function isRetriableFetchNetworkError ( error ) {
180+ return retriableFetchNetworkErrorCodes . has ( error ?. code ) ||
181+ retriableFetchNetworkErrorCodes . has ( error ?. cause ?. code )
182+ }
183+
184+ function waitForFetchRetry ( ) {
185+ return new Promise ( resolve => setTimeout ( resolve , 0 ) )
186+ }
187+
154188function handleFetchDone ( response ) {
155189 finalizeAndReportTiming ( response , 'fetch' )
156190}
@@ -1815,18 +1849,7 @@ async function httpNetworkFetch (
18151849) {
18161850 assert ( ! fetchParams . controller . connection || fetchParams . controller . connection . destroyed )
18171851
1818- fetchParams . controller . connection = {
1819- abort : null ,
1820- destroyed : false ,
1821- destroy ( err , abort = true ) {
1822- if ( ! this . destroyed ) {
1823- this . destroyed = true
1824- if ( abort ) {
1825- this . abort ?. ( err ?? new DOMException ( 'The operation was aborted.' , 'AbortError' ) )
1826- }
1827- }
1828- }
1829- }
1852+ createFetchConnection ( fetchParams )
18301853
18311854 // 1. Let request be fetchParams’s request.
18321855 const request = fetchParams . request
@@ -2000,6 +2023,22 @@ async function httpNetworkFetch (
20002023 response = makeResponse ( { status, statusText, headersList } )
20012024 }
20022025 } catch ( err ) {
2026+ if ( err ?. [ kFetchNetworkErrorRetry ] ) {
2027+ fetchParams . controller . connection . destroy ( err , false )
2028+
2029+ await waitForFetchRetry ( )
2030+
2031+ if ( isCancelled ( fetchParams ) ) {
2032+ return makeAppropriateNetworkError ( fetchParams , err )
2033+ }
2034+
2035+ if ( request . body != null ) {
2036+ request . body = safelyExtractBody ( request . body . source ) [ 0 ]
2037+ }
2038+
2039+ return httpNetworkFetch ( fetchParams , includeCredentials , true )
2040+ }
2041+
20032042 // 10. If aborted, then:
20042043 if ( err . name === 'AbortError' ) {
20052044 // 1. If connection uses HTTP/2, then transmit an RST_STREAM frame.
@@ -2376,6 +2415,18 @@ async function httpNetworkFetch (
23762415 return
23772416 }
23782417
2418+ if (
2419+ this . body == null &&
2420+ ! forceNewConnection &&
2421+ request . mode !== 'websocket' &&
2422+ canReplayRequestBody ( request ) &&
2423+ isRetriableFetchNetworkError ( error )
2424+ ) {
2425+ error [ kFetchNetworkErrorRetry ] = true
2426+ reject ( error )
2427+ return
2428+ }
2429+
23792430 this . body ?. destroy ( error )
23802431
23812432 fetchParams . controller . terminate ( error )
0 commit comments