@@ -947,6 +947,12 @@ export default class Request extends Duplex implements RequestEvents<Request> {
947947 }
948948 } ) ;
949949
950+ const noPipeCookieJarRawBodyPromise = this . _noPipe
951+ && is . object ( options . cookieJar )
952+ && ! ( response . headers . location && redirectCodes . has ( statusCode ) )
953+ ? this . _setRawBody ( response )
954+ : undefined ;
955+
950956 const rawCookies = response . headers [ 'set-cookie' ] ;
951957 if ( is . object ( options . cookieJar ) && rawCookies ) {
952958 let promises : Array < Promise < unknown > > = rawCookies . map ( async ( rawCookie : string ) => ( options . cookieJar as PromiseCookieJar ) . setCookie ( rawCookie , url ! . toString ( ) ) ) ;
@@ -1109,7 +1115,14 @@ export default class Request extends Duplex implements RequestEvents<Request> {
11091115 }
11101116
11111117 // Set up end listener AFTER redirect check to avoid emitting progress for redirect responses
1112- response . once ( 'end' , ( ) => {
1118+ let responseEndHandled = false ;
1119+ const handleResponseEnd = ( ) => {
1120+ if ( responseEndHandled ) {
1121+ return ;
1122+ }
1123+
1124+ responseEndHandled = true ;
1125+
11131126 // Validate content-length if it was provided
11141127 // Per RFC 9112: "If the sender closes the connection before the indicated number
11151128 // of octets are received, the recipient MUST consider the message to be incomplete"
@@ -1130,7 +1143,9 @@ export default class Request extends Duplex implements RequestEvents<Request> {
11301143 } ) ;
11311144
11321145 this . push ( null ) ;
1133- } ) ;
1146+ } ;
1147+
1148+ response . once ( 'end' , handleResponseEnd ) ;
11341149
11351150 this . emit ( 'downloadProgress' , this . downloadProgress ) ;
11361151
@@ -1149,7 +1164,14 @@ export default class Request extends Duplex implements RequestEvents<Request> {
11491164 } ) ;
11501165
11511166 if ( this . _noPipe ) {
1152- const success = await this . _setRawBody ( ) ;
1167+ const captureFromResponse = response . readableEnded || noPipeCookieJarRawBodyPromise !== undefined ;
1168+ const success = noPipeCookieJarRawBodyPromise
1169+ ? await noPipeCookieJarRawBodyPromise
1170+ : await this . _setRawBody ( captureFromResponse ? response : this ) ;
1171+
1172+ if ( captureFromResponse ) {
1173+ handleResponseEnd ( ) ;
1174+ }
11531175
11541176 if ( success ) {
11551177 this . emit ( 'response' , response ) ;
@@ -1192,10 +1214,6 @@ export default class Request extends Duplex implements RequestEvents<Request> {
11921214 }
11931215
11941216 private async _setRawBody ( from : Readable = this ) : Promise < boolean > {
1195- if ( from . readableEnded ) {
1196- return false ;
1197- }
1198-
11991217 try {
12001218 // Errors are emitted via the `error` event
12011219 const fromArray = await from . toArray ( ) ;
@@ -1212,6 +1230,9 @@ export default class Request extends Duplex implements RequestEvents<Request> {
12121230 && this . response
12131231 ) {
12141232 this . response . rawBody = rawBody ;
1233+ if ( from !== this ) {
1234+ this . _downloadedSize = rawBody . byteLength ;
1235+ }
12151236
12161237 if ( shouldUseIncrementalDecodedBody ) {
12171238 try {
@@ -1305,7 +1326,9 @@ export default class Request extends Duplex implements RequestEvents<Request> {
13051326 }
13061327
13071328 /*
1308- Transient write errors (EPIPE, ECONNRESET) often fire during redirects when the server closes the connection after sending the redirect response. Defer by one microtask to let the response event make the request stale.
1329+ Transient write errors (EPIPE, ECONNRESET) often fire during redirects when the
1330+ server closes the connection after sending the redirect response. Defer by one
1331+ microtask to let the response event make the request stale.
13091332 */
13101333 if ( isTransientWriteError ( error ) ) {
13111334 queueMicrotask ( ( ) => {
@@ -1419,25 +1442,28 @@ export default class Request extends Duplex implements RequestEvents<Request> {
14191442 const isInitialRequest = currentRequest === this ;
14201443
14211444 ( async ( ) => {
1445+ let request : ClientRequest | undefined ;
1446+
14221447 try {
1423- const request = isInitialRequest ? this . _request : currentRequest as ClientRequest ;
1448+ request = isInitialRequest ? this . _request : currentRequest as ClientRequest ;
1449+ const activeRequest = request ;
14241450
1425- if ( ! request ) {
1451+ if ( ! activeRequest ) {
14261452 if ( isInitialRequest ) {
14271453 super . end ( ) ;
14281454 }
14291455
14301456 return ;
14311457 }
14321458
1433- if ( request . destroyed ) {
1459+ if ( activeRequest . destroyed ) {
14341460 return ;
14351461 }
14361462
1437- await this . _writeChunksToRequest ( buffer , request ) ;
1463+ await this . _writeChunksToRequest ( buffer , activeRequest ) ;
14381464
1439- if ( this . _isRequestStale ( request ) ) {
1440- this . _finalizeStaleChunkedWrite ( request , isInitialRequest ) ;
1465+ if ( this . _isRequestStale ( activeRequest ) ) {
1466+ this . _finalizeStaleChunkedWrite ( activeRequest , isInitialRequest ) ;
14411467 return ;
14421468 }
14431469
@@ -1447,25 +1473,54 @@ export default class Request extends Duplex implements RequestEvents<Request> {
14471473 }
14481474
14491475 await new Promise < void > ( ( resolve , reject ) => {
1450- request . end ( ( error ?: Error | null ) => { // eslint-disable-line @typescript-eslint/no-restricted-types
1476+ activeRequest . end ( ( error ?: Error | null ) => { // eslint-disable-line @typescript-eslint/no-restricted-types
14511477 if ( error ) {
14521478 reject ( error ) ;
14531479 return ;
14541480 }
14551481
1456- if ( this . _request === request && ! request . destroyed ) {
1457- this . _emitUploadComplete ( request ) ;
1482+ if ( this . _request === activeRequest && ! activeRequest . destroyed ) {
1483+ this . _emitUploadComplete ( activeRequest ) ;
14581484 }
14591485
14601486 resolve ( ) ;
14611487 } ) ;
14621488 } ) ;
14631489 } catch ( error : unknown ) {
1490+ const normalizedError = normalizeError ( error ) ;
1491+
1492+ // Transient write errors (EPIPE, ECONNRESET) are handled by the request-level
1493+ // error and close handlers. For initial redirected writes, still finalize
1494+ // writable state once the stale transition becomes observable.
1495+ if ( isTransientWriteError ( normalizedError ) ) {
1496+ if ( isInitialRequest && request ) {
1497+ const initialRequest = request ;
1498+ let didFinalize = false ;
1499+ const finalizeIfStale = ( ) => {
1500+ if ( didFinalize || ! this . _isRequestStale ( initialRequest ) ) {
1501+ return ;
1502+ }
1503+
1504+ didFinalize = true ;
1505+ this . _finalizeStaleChunkedWrite ( initialRequest , true ) ;
1506+ } ;
1507+
1508+ finalizeIfStale ( ) ;
1509+
1510+ if ( ! didFinalize ) {
1511+ initialRequest . once ( 'response' , finalizeIfStale ) ;
1512+ queueMicrotask ( finalizeIfStale ) ;
1513+ }
1514+ }
1515+
1516+ return ;
1517+ }
1518+
14641519 if ( ! isInitialRequest && this . _isRequestStale ( currentRequest as ClientRequest ) ) {
14651520 return ;
14661521 }
14671522
1468- this . _beforeError ( normalizeError ( error ) ) ;
1523+ this . _beforeError ( normalizedError ) ;
14691524 }
14701525 } ) ( ) ;
14711526 }
@@ -1716,7 +1771,8 @@ export default class Request extends Duplex implements RequestEvents<Request> {
17161771
17171772 // Set cookies
17181773 if ( cookieJar ) {
1719- const cookieString : string = await cookieJar . getCookieString ( options . url ! . toString ( ) ) ;
1774+ let cookieString = '' ;
1775+ cookieString = await cookieJar . getCookieString ( options . url ! . toString ( ) ) ;
17201776
17211777 if ( is . nonEmptyString ( cookieString ) ) {
17221778 options . setInternalHeader ( 'cookie' , cookieString ) ;
0 commit comments