stream: allow null as second arg in Transform callback#62803
stream: allow null as second arg in Transform callback#62803galaxy4276 wants to merge 2 commits intonodejs:mainfrom
Conversation
callback(null, null) in a Transform._transform method should be equivalent to calling this.push(null) followed by callback(), ending the readable side of the stream. Previously val != null blocked the push because null == null is true in loose equality, so push(null) was never called and the stream never emitted 'end'. Change the guard from `val != null` to `val !== undefined` so that null passes through to this.push(), which sets state.ended and eventually emits 'end', matching documented behavior. Fixes: nodejs#62769 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Review requested:
|
|
Looking at the history, The fix changes to
In practice, That said, I'm uncertain whether this is semver-patch (bug fix aligning with docs) or semver-minor (observable behavior change). Would appreciate guidance on the right label and whether a CHANGELOG note is needed. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #62803 +/- ##
==========================================
- Coverage 89.70% 89.69% -0.01%
==========================================
Files 706 706
Lines 218288 218222 -66
Branches 41782 41767 -15
==========================================
- Hits 195806 195742 -64
+ Misses 14394 14381 -13
- Partials 8088 8099 +11
🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mcollina
left a comment
There was a problem hiding this comment.
This should have a pass of citgm before landing.
I expect a significant breakage in the ecosystem.
Thank you for your review. |
citgm run — resultsTL;DR: Full citgm-all + extended stream subset + pattern audit show zero ecosystem regressions. Environment
Full citgm-all (72 modules)
Pre-existing failures (identical on both binaries)
All fail with the same error on both binaries (yarn resolution, native addon build on Apple clang 21, environment/flake). Spot-checked Extended stream subset (17 modules not in citgm lookup)Ran against
Pattern auditGrepped
Hits outside Transform context: webpack (7), undici (2), mongoose (8) — all in unrelated async callbacks (resolver cache, dispatcher, cursor), not Hits inside Transform context: 0. Semantic differentialConfirmed the change's behavior is observable with a focused test: const t = new Transform({
transform(chunk, enc, cb) {
if (chunk.toString() === 'stop') cb(null, null);
else cb(null, chunk);
},
});
t.on('data', (d) => received.push(d));
t.write('a'); t.write('stop'); t.write('b'); t.end();
So the change is a real semantic change — but we found no library in the tested universe that relies on it. Applications using Conclusion & SemVer recommendation
Given the pattern is not observed in the ecosystem but is a runtime-visible change, I'd suggest Alternatives considered
Raw artifactsAvailable on request — stdout logs, per-module TAP, and pattern-hits file can be uploaded to a Gist if useful. Notes
|
What
The docs say passing a value as the second argument to the transform callback is equivalent to calling
transform.push(value). That impliescallback(null, null)should pushnull— ending the readable side — just likethis.push(null); callback()does. It doesn't.Bug
The
'end'event never fires. The stream hangs.Root cause —
lib/internal/streams/transform.js:val != nulluses loose equality, so bothnullandundefinedfail the check.this.push(null)is never called,state.endedstaysfalse, and'end'is never emitted.Fix
Now
callback(null, null)reachesthis.push(null), which setsstate.ended = trueand eventually emits'end'once the buffer drains — matching what the docs describe.Behavior after fix
Potential concern
This is technically a behavior change. Code that relied on
callback(null, null)not ending the stream should usecallback()(no second argument) instead. I think this is the right call given the docs, but open to pushback — especially on whether this warrants a semver-minor label or a deprecation note before changing.