Skip to content

fix: recover complete request bodies after client disconnect#375

Open
usualoma wants to merge 5 commits into
mainfrom
fix/client-disconnect-body-recovery
Open

fix: recover complete request bodies after client disconnect#375
usualoma wants to merge 5 commits into
mainfrom
fix/client-disconnect-body-recovery

Conversation

@usualoma

Copy link
Copy Markdown
Member

fixes #370

A client disconnect marks the IncomingMessage as disturbed even when Node's HTTP/1 parser has already received the complete request body. As a result, a later body read can incorrectly fail with Body is unusable.

This follows the same core approach as #373:

  • Use incoming.readableDidRead instead of Readable.isDisturbed(incoming) to determine whether the body was actually consumed.
  • If the HTTP/1 request was completed before the disconnect, recover the untouched body retained in the incoming stream's internal buffer.

The main difference is that the recovery is shared across the request body-reading paths rather than being limited to readBodyDirect. This also covers formData(), c.req.raw.body, and paths that create or use the native Request cache.

Although the diff is relatively large, the core behavior is the same. Most of the additional code and tests ensure that recovery is applied consistently while rejecting partially consumed bodies, preserving application-provided stream errors, and avoiding byte corruption when setEncoding() has been used.

usualoma and others added 4 commits July 14, 2026 05:56
When application code has called setEncoding() on the IncomingMessage, data events yield strings in that encoding. Converting those chunks back to bytes as UTF-8 corrupts bodies decoded with another encoding.

Decode with incoming.readableEncoding and cover the streamed latin1 case.
A client can disconnect after Node has parsed the complete HTTP/1 request but before the application reads it. Recover the untouched buffered body for direct reads, native Request reads, formData(), and raw body streams.

Share and cache the recovery result across paths, preserve application errors, reject lossy string encodings, and validate canonical content-length values. Add real-socket and synthetic coverage with bounded waits so regressions fail without leaking the test server.
Inspired by #373.

Co-authored-by: BlankParticle <blankparticle@gmail.com>
Adopts the normalizeAbortError helper from
#373.

Co-authored-by: BlankParticle <blankparticle@gmail.com>
Comment thread src/request.ts
const isRecoverableDisconnectedIncoming = (
incoming: IncomingMessage | Http2ServerRequest
): incoming is IncomingMessage =>
!(incoming instanceof Http2ServerRequest) &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a fan of instanceof checks
because the input is either a http/1 or http/2 request, I think incoming.httpVersionMajor === 1 is a much better check

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I do understand your preference, since we've basically been using incoming instanceof Http2ServerRequest throughout this project, I think it would be more appropriate to use incoming instanceof Http2ServerRequest here as well.

@BlankParticle BlankParticle Jul 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's a pattern already being used here, then I guess it's fine doing it here too
I just have a distrust of instanceof checks because libs including hono replaces the globals and these kind of checks fail in application code

Comment thread src/request.ts Outdated
const recoverCompleteBodyAfterDisconnect = (error?: unknown): boolean => {
const streamError = incoming.errored ?? error
if (
incoming instanceof Http2ServerRequest ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should use the existing isRecoverableDisconnectedIncoming here and add the last condition with it

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. I've incorporated it. c86c3e1

Co-authored-by: BlankParticle <blankparticle@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v2: body read after client disconnect rejects with TypeError: Body is unusable even when the full body was received (regression from v1)

2 participants