Skip to content

fix: do not end parts() iteration while busboy still holds undelivered parts#631

Merged
mcollina merged 1 commit into
mainfrom
fix-630
Jul 6, 2026
Merged

fix: do not end parts() iteration while busboy still holds undelivered parts#631
mcollina merged 1 commit into
mainfrom
fix-630

Conversation

@mcollina

@mcollina mcollina commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #630.

request.on('close') fires once the message body has fully drained into busboy's writable buffer. Busboy defers its write ack when a file stream backs up, so on a fast/re-segmented connection the request can complete while busboy still holds parsed-but-undelivered parts. The cleanup bound to close pushed the end marker into the same queue the parts flow through, and every part emitted afterwards landed behind it — trailing file parts and fields were silently lost, exactly as reported.

Changes

  • End iteration on busboy's own end events. request.on('close') only terminates the iteration when the message did not complete (readableEnded === false, i.e. a client abort), and then with a new PrematureCloseError (FST_MP_PREMATURE_CLOSE, exposed via fastify.multipartErrors and the TypeScript types) instead of a silent clean end.
  • Surface Unexpected end of multipart data as an error. The suppression came in mechanically with the busboy v2.1.0 upgrade (use @fastify/busboy v2.1.0 #506); it made truncated uploads indistinguishable from complete ones. A fully empty body (zero bytes received) is still treated as an empty form, preserving the behavior pinned by multipart-empty-body.test.js.
  • End iteration when the consumer destroys a file stream without reading it to the end. Busboy never emits finish in that case; previously the close-based cleanup masked this, and without a guard parts() would hang forever (caught by the existing should not freeze when error is thrown during processing test).

Reproduction / tests

Written TDD-style in test/fix-630.test.js (red on main, green here):

  1. Trailing parts survive early request close — slow consumer, 200 KB file + 6 KB mask + three fields. A large busboy highWaterMark makes the failing event ordering deterministic on loopback (the same ordering the issue reproduced through Docker Desktop's port forwarding with default settings); on main the handler sees only the first file.
  2. Truncated multipart data rejects with Unexpected end of multipart data instead of ending cleanly.
  3. Client abort mid-upload rejects the iterator — guards that dropping the close-based cleanup doesn't reintroduce hangs on aborts.

Behavioral note

Consumers that previously saw a clean-looking, silently-truncated iteration on truncated or aborted uploads now get an error — that is the point of the fix, but it may warrant a semver-major release.

Checklist

  • run npm run test — 90/90 unit tests, 100 % coverage, tstyche 34/34
  • tests and/or benchmarks are included
  • documentation is changed or added — no doc changes needed (error is exported via existing multipartErrors mechanism)
  • commit message follows commit guidelines

🤖 Generated with Claude Code

…d parts

The request 'close' event fires once the message body has fully drained
into busboy's writable buffer, which can happen while the consumer is
still reading an earlier file part and busboy is still holding parsed
but undelivered parts. The cleanup bound to that event pushed the end
marker into the same queue the parts flow through, so trailing parts
were silently lost.

- end iteration on busboy's own end events; request 'close' only ends
  it when the message did not complete (client abort), and then with a
  new PrematureCloseError instead of a silent clean end
- surface busboy's 'Unexpected end of multipart data' as an error so
  truncated multipart data is never mistaken for a complete iteration;
  a fully empty body is still treated as an empty form
- end iteration when the consumer destroys a file stream without
  reading it to the end, since busboy will never emit 'finish' then

Fixes #630

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mcollina mcollina requested a review from gurgunday July 6, 2026 16:56

@gurgunday gurgunday left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Lgtm

@mcollina mcollina merged commit 9b3bf89 into main Jul 6, 2026
17 checks passed
@mcollina mcollina deleted the fix-630 branch July 6, 2026 22:52
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.

parts() can end iteration while parts remain undelivered (trailing file parts silently lost on reused connections)

2 participants