You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
http.request({ highWaterMark: N }) passes N to the TCP socket but does not set it on the OutgoingMessage's internal kHighWaterMark. This causes two problems:
The user's highWaterMark is silently ignored for writes before the socket connects (all Node versions). write() returns the wrong boolean because _writeRaw Path B checks outputSize < this[kHighWaterMark] which remains at the default 64KB.
Deadlock on Node >= 24.16.0 (post #62936): Because write() incorrectly returns false, kNeedDrain is set. The stricter drain semantics from http: emit 'drain' on OutgoingMessage only after buffers drain #62936 require the socket to emit drain — but the socket was never backpressured (the data is below the socket's HWM). Result: drain never fires, the function hangs forever.
Minimal reproduction
# Terminal 1 — start a simple server
node -e "require('http').createServer((req, res) => { req.resume(); req.on('end', () => res.end('ok')); }).listen(9001)"
// Terminal 2 — repro.jsconsthttp=require('http');constreq=http.request({hostname: '127.0.0.1',port: 9001,method: 'POST',highWaterMark: 100_000,// 100KB — should allow 64KB writes without backpressure});constok=req.write(Buffer.alloc(64*1024));console.log(`write() returned ${ok}, expected true (64KB < 100KB highWaterMark)`);if(!ok){setTimeout(()=>{console.error('DEADLOCK: drain never fired');process.exit(1);},3000);req.on('drain',()=>{console.log('drain fired');req.end();});}else{req.end();req.on('response',(res)=>{res.resume();res.on('end',()=>process.exit(0));});}
Results
Node version
write() returns
Drain fires?
Outcome
v22.22.2
false (wrong, expected true)
Yes (old eager drain)
HWM ignored but no hang
v26.5.0
false (wrong, expected true)
Never
DEADLOCK
Root cause
OutgoingMessage._writeRaw() has two mutually exclusive paths:
Path A (socket connected): conn.write(data) — backpressure from socket's writableHighWaterMark (correct)
Path B (socket not yet connected): outputSize < this[kHighWaterMark] — backpressure from OM's own property (incorrect, stuck at default)
The OutgoingMessage constructor already accepts options.highWaterMark:
But ClientRequest in lib/_http_client.js never passes the user's highWaterMark to the OutgoingMessage super constructor — it only passes it to createConnection() for the socket.
Expected behavior
write() should return true when the written data is below the user-specified highWaterMark, regardless of whether the socket has connected yet.
Proposed fix
Propagate the user's highWaterMark option to OutgoingMessage in the ClientRequest constructor so both write paths respect the same threshold.
What is the bug?
http.request({ highWaterMark: N })passesNto the TCP socket but does not set it on theOutgoingMessage's internalkHighWaterMark. This causes two problems:The user's
highWaterMarkis silently ignored for writes before the socket connects (all Node versions).write()returns the wrong boolean because_writeRawPath B checksoutputSize < this[kHighWaterMark]which remains at the default 64KB.Deadlock on Node >= 24.16.0 (post #62936): Because
write()incorrectly returnsfalse,kNeedDrainis set. The stricter drain semantics from http: emit 'drain' on OutgoingMessage only after buffers drain #62936 require the socket to emitdrain— but the socket was never backpressured (the data is below the socket's HWM). Result:drainnever fires, the function hangs forever.Minimal reproduction
Results
write()returnsfalse(wrong, expectedtrue)false(wrong, expectedtrue)Root cause
OutgoingMessage._writeRaw()has two mutually exclusive paths:conn.write(data)— backpressure from socket'swritableHighWaterMark(correct)outputSize < this[kHighWaterMark]— backpressure from OM's own property (incorrect, stuck at default)The
OutgoingMessageconstructor already acceptsoptions.highWaterMark:But
ClientRequestinlib/_http_client.jsnever passes the user'shighWaterMarkto theOutgoingMessagesuper constructor — it only passes it tocreateConnection()for the socket.Expected behavior
write()should returntruewhen the written data is below the user-specifiedhighWaterMark, regardless of whether the socket has connected yet.Proposed fix
Propagate the user's
highWaterMarkoption toOutgoingMessagein theClientRequestconstructor so both write paths respect the same threshold.Refs: #62936, #39092, #32781