diff --git a/lib/_http_client.js b/lib/_http_client.js index 9eb3c10547d520..2cdc840b60fea7 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -51,6 +51,7 @@ const { kSkipPendingData, } = require('_http_common'); const { + kHighWaterMark, kUniqueHeaders, parseUniqueHeadersOption, OutgoingMessage, @@ -348,6 +349,14 @@ function ClientRequest(input, options, cb) { options = ObjectAssign({ __proto__: null }, input, options); } + // Propagate the user's highWaterMark to OutgoingMessage so that + // _writeRaw() uses the correct threshold for writes buffered before + // the socket connects (Path B). Without this, the OutgoingMessage + // defaults to 64 KB regardless of what the caller requested. + if (options.highWaterMark != null) { + this[kHighWaterMark] = options.highWaterMark; + } + let agent = options.agent; const defaultAgent = options._defaultAgent || Agent.globalAgent; if (agent === false) { diff --git a/test/parallel/test-http-client-highwatermark.js b/test/parallel/test-http-client-highwatermark.js new file mode 100644 index 00000000000000..b36d6dc54bb01b --- /dev/null +++ b/test/parallel/test-http-client-highwatermark.js @@ -0,0 +1,81 @@ +// Flags: --expose-internals +'use strict'; + +// Regression test: http.request({ highWaterMark }) must propagate the value +// to OutgoingMessage's kHighWaterMark so that _writeRaw() Path B (buffering +// before socket connects) uses the correct threshold. +// +// Without the fix: +// - write() returns the wrong boolean (compares against default 64KB) +// - On Node >= 24.16.0 (post #62936), this causes a deadlock when +// the user awaits 'drain' after write() incorrectly returns false. +// +// Fixes: https://github.com/nodejs/node/issues/64645 + +const common = require('../common'); +const assert = require('assert'); +const http = require('http'); +const { kHighWaterMark } = require('_http_outgoing'); +const { getDefaultHighWaterMark } = require('internal/streams/state'); + +const server = http.createServer(common.mustCall((req, res) => { + req.resume(); + req.on('end', () => res.end('ok')); +}, 3)); + +server.listen(0, common.mustCall(() => { + const port = server.address().port; + let completed = 0; + + function done() { + if (++completed === 3) server.close(); + } + + // Test 1: kHighWaterMark is set to user value on the ClientRequest. + { + const hwm = getDefaultHighWaterMark() * 2; + const req = http.request({ port, method: 'POST', highWaterMark: hwm }); + assert.strictEqual(req[kHighWaterMark], hwm); + req.end(); + req.on('response', (res) => { res.resume(); res.on('end', done); }); + } + + // Test 2: Large HWM — write below threshold returns true before socket connects. + { + const req = http.request({ + port, + method: 'POST', + highWaterMark: 100_000, + }, common.mustCall((res) => { + res.resume(); + res.on('end', done); + })); + + // 64KB write in the same tick — socket not yet connected (Path B). + // With HWM=100KB, write() must return true. + const result = req.write(Buffer.alloc(64 * 1024)); + assert.strictEqual(result, true); + req.end(); + } + + // Test 3: Small HWM — write above threshold returns false, drain fires. + { + const req = http.request({ + port, + method: 'POST', + highWaterMark: 512, + }, common.mustCall((res) => { + res.resume(); + res.on('end', done); + })); + + // 2KB write in the same tick — exceeds HWM of 512 bytes. + const result = req.write(Buffer.alloc(2 * 1024)); + assert.strictEqual(result, false); + + // Drain must fire (no deadlock) so we can complete the request. + req.on('drain', common.mustCall(() => { + req.end(); + })); + } +}));