Skip to content

fix(readable): consume a body whose end has already been emitted - #5617

Merged
mcollina merged 1 commit into
nodejs:mainfrom
marko1olo:fix/consume-start-ended-body
Jul 29, 2026
Merged

fix(readable): consume a body whose end has already been emitted#5617
mcollina merged 1 commit into
nodejs:mainfrom
marko1olo:fix/consume-start-ended-body

Conversation

@marko1olo

Copy link
Copy Markdown
Contributor

Fixes #5614.

consumeStart() is a free function, called as consumeStart(stream[kConsume]) from the queueMicrotask() above it, so this is undefined inside it under 'use strict'. The state.endEmitted branch read this[kConsume] anyway:

TypeError: Cannot read properties of undefined (reading 'Symbol(kConsume)')
    at consumeStart (lib/api/readable.js:490:20)
    at lib/api/readable.js:450:9
    at node:internal/process/task_queues:149:7

The two lines are character-for-character the same as the ones in the on('end') callback immediately below, which is what makes it easy to read past — those work, because the emitter invokes the callback with the stream as its receiver.

The part that makes it worth fixing rather than tidying is that the caller cannot catch it. The throw happens on a microtask rather than in the promise chain, so awaiting the consume inside try/catch still takes the process down. Draining a body and then deciding to read it after all is enough to reach it.

On the early return

Correcting only the receiver moves the crash rather than removing it:

TypeError: Cannot read properties of null (reading 'resume')
    at consumeStart (lib/api/readable.js:497:18)

consumeEnd() sets consume.stream to null, and the consume.stream.resume() a few lines down then dereferences it — reachable only once the branch above stops throwing first. So the return is part of the fix.

Tests

One case added to test/readable.js: an empty body, drained, then consumed. Without the change the test runner reports it as an uncaught exception rather than a failed assertion, which is the same shape as the bug:

Error: A resource generated asynchronous activity after the test ended. This activity created the
error "TypeError: Cannot read properties of undefined (reading 'Symbol(kConsume)')" which triggered
an uncaughtException event, caught by the test runner.

test/readable.js 11/11 and test/client-request.js 48/48 with the change; eslint clean.

Possible drawbacks

The endEmitted path is reached by consuming a body after 'end', and I have only exercised it with an empty body and with a drained 204 over a real request. A body that emitted data first is disturbed and rejects with TypeError: unusable before reaching this code, both before and after — I checked that separately so the change is not masking it.

Node 24 on Windows only; I have not run the other matrix cells locally. I did not run npm run test:typescript, which fails on clean main in Git Bash for an unrelated reason (the script relies on shell glob expansion for types/*.d.ts).

consumeStart() is a free function, invoked as consumeStart(stream[kConsume]) from
the queueMicrotask() above it, so there is no `this` inside it. The endEmitted
branch nevertheless read this[kConsume] and this._readableState.encoding:

    TypeError: Cannot read properties of undefined (reading 'Symbol(kConsume)')
        at consumeStart (lib/api/readable.js:490:20)
        at lib/api/readable.js:450:9
        at node:internal/process/task_queues:149:7

The two lines are identical to the ones in the on('end') callback just below,
where they are correct because the emitter supplies the stream as the receiver.

Because the throw happens on a microtask rather than in the promise chain, the
caller cannot catch it: awaiting the consume inside try/catch still ends the
process. Draining a body and then deciding to read it is enough to get there,
which the added test does with an empty body.

The early return is part of the fix rather than tidying: consumeEnd() clears
consume.stream, and the resume() a few lines down would then dereference null.

Signed-off-by: marko1olo <marko1olo@users.noreply.github.com>
@codecov-commenter

codecov-commenter commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.51%. Comparing base (1089808) to head (c07ef96).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #5617    +/-   ##
========================================
  Coverage   93.50%   93.51%            
========================================
  Files         110      110            
  Lines       38303    38429   +126     
========================================
+ Hits        35816    35935   +119     
- Misses       2487     2494     +7     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mcollina mcollina 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 57f50a2 into nodejs:main Jul 29, 2026
37 of 38 checks passed
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.

Consuming an already-ended response body throws an uncatchable TypeError from consumeStart()

3 participants