@@ -261,6 +261,40 @@ const readBodyWithFastPath = <T>(
261261 } )
262262}
263263
264+ const normalizeAbortError = (
265+ request : Record < string | symbol , any > ,
266+ incoming : IncomingMessage | Http2ServerRequest
267+ ) : Error => {
268+ if ( incoming . errored ) {
269+ return incoming . errored
270+ }
271+ const reason = request [ abortReasonKey ]
272+ if ( typeof reason !== 'undefined' ) {
273+ return reason instanceof Error ? reason : new Error ( String ( reason ) )
274+ }
275+ return new Error ( 'Client connection prematurely closed.' )
276+ }
277+
278+ // A client abort destroys the IncomingMessage, but does not clear its internal
279+ // buffer. For HTTP/1, `complete === true` guarantees the parser received the
280+ // entire message, so the unread body can still be drained synchronously.
281+ // (Not valid for HTTP/2, where `complete` is also true for aborted requests. see https://nodejs.org/api/http2.html#requestcomplete)
282+ const canBeRecoveredAfterAbort = (
283+ incoming : IncomingMessage | Http2ServerRequest
284+ ) : incoming is IncomingMessage => {
285+ return incoming . complete && incoming . httpVersionMajor === 1
286+ }
287+
288+ const readIncomingMessageInternalBuffer = ( incoming : IncomingMessage ) => {
289+ const chunks : Buffer [ ] = [ ]
290+ let chunk : Buffer | string | null
291+ while ( ( chunk = incoming . read ( ) ) !== null ) {
292+ chunks . push ( Buffer . isBuffer ( chunk ) ? chunk : Buffer . from ( chunk ) )
293+ }
294+ const buffer = chunks . length === 1 ? chunks [ 0 ] : Buffer . concat ( chunks )
295+ return buffer
296+ }
297+
264298const readRawBodyIfAvailable = ( request : Record < string | symbol , any > ) : Buffer | undefined => {
265299 const incoming = request [ incomingKey ] as IncomingMessage | Http2ServerRequest
266300 if ( 'rawBody' in incoming && ( incoming as any ) . rawBody instanceof Buffer ) {
@@ -283,7 +317,16 @@ const readBodyDirect = (request: Record<string | symbol, any>): Promise<Buffer>
283317
284318 const incoming = request [ incomingKey ] as IncomingMessage | Http2ServerRequest
285319 if ( Readable . isDisturbed ( incoming ) ) {
286- return rejectBodyUnusable ( )
320+ if ( incoming . readableDidRead ) {
321+ return rejectBodyUnusable ( )
322+ }
323+ // Aborted (e.g. client disconnect) before anything read the stream.
324+ if ( canBeRecoveredAfterAbort ( incoming ) ) {
325+ const buffer = readIncomingMessageInternalBuffer ( incoming )
326+ request [ bodyBufferKey ] = buffer
327+ return Promise . resolve ( buffer )
328+ }
329+ return Promise . reject ( normalizeAbortError ( request , incoming ) )
287330 }
288331
289332 const promise = new Promise < Buffer > ( ( resolve , reject ) => {
@@ -319,17 +362,14 @@ const readBodyDirect = (request: Record<string | symbol, any>): Promise<Buffer>
319362 onEnd ( )
320363 return
321364 }
365+ if ( canBeRecoveredAfterAbort ( incoming ) ) {
366+ // Drain what the abort left in the internal buffer.
367+ readIncomingMessageInternalBuffer ( incoming )
368+ onEnd ( )
369+ return
370+ }
322371 finish ( ( ) => {
323- if ( incoming . errored ) {
324- reject ( incoming . errored )
325- return
326- }
327- const reason = request [ abortReasonKey ]
328- if ( reason !== undefined ) {
329- reject ( reason instanceof Error ? reason : new Error ( String ( reason ) ) )
330- return
331- }
332- reject ( new Error ( 'Client connection prematurely closed.' ) )
372+ reject ( normalizeAbortError ( request , incoming ) )
333373 } )
334374 }
335375 const cleanup = ( ) => {
0 commit comments