From 972f7330d38da9469ac36a013c6128e39c6f612f Mon Sep 17 00:00:00 2001 From: ThierryMT Date: Mon, 27 Oct 2025 15:32:41 +0000 Subject: [PATCH 1/2] http: fix drain event with cork/uncork When using cork() and uncork() with ServerResponse, the drain event was not reliably emitted after uncorking. This occurred because the uncork() method did not check if a drain was pending (kNeedDrain flag) after flushing the chunked buffer. This fix ensures that when uncork() successfully flushes buffered data and a drain was needed, the drain event is emitted immediately. Fixes: #60432 --- lib/_http_outgoing.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 24aae1caca69d3..7799e62c62c48f 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -287,7 +287,7 @@ OutgoingMessage.prototype.uncork = function uncork() { callbacks.push(buf[n + 2]); } } - this._send(crlf_buf, null, callbacks.length ? (err) => { + const ret = this._send(crlf_buf, null, callbacks.length ? (err) => { for (const callback of callbacks) { callback(err); } @@ -295,6 +295,12 @@ OutgoingMessage.prototype.uncork = function uncork() { this[kChunkedBuffer].length = 0; this[kChunkedLength] = 0; + + // If we successfully flushed and had pending drain, emit it + if (ret && this[kNeedDrain]) { + this[kNeedDrain] = false; + this.emit('drain'); + } }; OutgoingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { From 9ab4cc4b6a610ac5374f76f36029073aa93523d5 Mon Sep 17 00:00:00 2001 From: ThierryMT Date: Tue, 28 Oct 2025 16:20:26 +0000 Subject: [PATCH 2/2] test: add test for drain event with cork/uncork Add test to verify that the drain event is properly emitted when using cork() and uncork() with ServerResponse. --- .../parallel/test-http-response-drain-cork.js | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 test/parallel/test-http-response-drain-cork.js diff --git a/test/parallel/test-http-response-drain-cork.js b/test/parallel/test-http-response-drain-cork.js new file mode 100644 index 00000000000000..78af133a055bc6 --- /dev/null +++ b/test/parallel/test-http-response-drain-cork.js @@ -0,0 +1,70 @@ +'use strict'; +const common = require('../common'); +const http = require('http'); +const assert = require('assert'); + +// Test that drain event is emitted correctly when using cork/uncork +// with ServerResponse and the write buffer is full + +const server = http.createServer(common.mustCall(async (req, res) => { + res.cork(); + + // Write small amount - won't need drain + assert.strictEqual(res.write('1'.repeat(100)), true); + + // Write large amount that should require drain + const needsDrain = !res.write('2'.repeat(1000000)); + + if (needsDrain) { + // Verify writableNeedDrain is set + assert.strictEqual(res.writableNeedDrain, true); + + // Wait for drain event after uncorking + const drainPromise = new Promise((resolve) => { + res.once('drain', common.mustCall(() => { + // After drain, writableNeedDrain should be false + assert.strictEqual(res.writableNeedDrain, false); + resolve(); + })); + }); + + // Uncork should trigger drain if needed + res.uncork(); + + await drainPromise; + + // Cork again for next write + res.cork(); + } + + // Write more data + res.write('3'.repeat(100)); + + // Final uncork and end + res.uncork(); + res.end(); +})); + +server.listen(0, common.mustCall(() => { + http.get({ + port: server.address().port, + }, common.mustCall((res) => { + let data = ''; + res.setEncoding('utf8'); + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', common.mustCall(() => { + // Verify we got all the data + assert.strictEqual(data.length, 100 + 1000000 + 100); + assert.strictEqual(data.substring(0, 100), '1'.repeat(100)); + assert.strictEqual(data.substring(100, 1000100), '2'.repeat(1000000)); + assert.strictEqual(data.substring(1000100), '3'.repeat(100)); + + server.close(); + })); + })); +})); +