Bug Description
Calling body.json() (or other consume helpers) and then tearing down the stream (abort/timeout/close) runs consumeFinish(), which sets consume.body = null. stream[kConsume] is left in place.
If BodyReadable.push() runs after that - for example a late chunk from the decompress interceptor- it still calls consumePush(), which does consume.body.push(chunk) and throws:
TypeError: Cannot read properties of null (reading 'push')
at consumePush (lib/api/readable.js:549)
This is a synchronous throw, not the AbortError from the .json() promise.
Reproducible By
const { Readable: BodyReadable } = require('undici/lib/api/readable');
describe('undici BodyReadable consume race', () => {
it('must throw when a chunk arrives after consume teardown', async () => {
const body = new BodyReadable({ resume: () => {}, abort: () => {} });
const jsonPromise = body.json();
await new Promise(resolve => queueMicrotask(resolve));
body.emit('close');
expect(() => body.push(Buffer.from('late chunk'))).toThrow(/Cannot read properties of null \(reading 'push'\)/);
await expect(jsonPromise).rejects.toMatchObject({ name: 'AbortError' });
});
});
in real usage: request() + interceptors.decompress(), body.json() with a short AbortSignal timeout while the response is still streaming
Expected Behavior
Late chunks after consume teardown should be dropped. .json() should reject with AbortError only - no extra sync crash.
Logs & Screenshots
TypeError: Cannot read properties of null (reading 'push')
at consumePush (/.../undici/lib/api/readable.js:549:16)
at BodyReadable.push (/.../undici/lib/api/readable.js:160:9)
Environment
- undici 7.27.0
- Node.js v24.12.0
- macOS / Linux, HTTP/1.1
- Agent.compose(interceptors.decompress())
Additional context
Looks related to #4880 (null guard for late push after H2 teardown). consumePush has no similar guard.
Bug Description
Calling
body.json()(or other consume helpers) and then tearing down the stream (abort/timeout/close) runsconsumeFinish(), which setsconsume.body = null.stream[kConsume]is left in place.If
BodyReadable.push()runs after that - for example a late chunk from the decompress interceptor- it still callsconsumePush(), which doesconsume.body.push(chunk)and throws:This is a synchronous throw, not the AbortError from the .json() promise.
Reproducible By
in real usage:
request()+interceptors.decompress(),body.json()with a short AbortSignal timeout while the response is still streamingExpected Behavior
Late chunks after consume teardown should be dropped.
.json()should reject with AbortError only - no extra sync crash.Logs & Screenshots
Environment
Additional context
Looks related to #4880 (null guard for late push after H2 teardown).
consumePushhas no similar guard.