Repository navigation
drain event is unreliable when using cork/uncork with ServerResponse #60432
Description
Version
v24.10.0
Platform
Darwin blah.local 24.6.0 Darwin Kernel Version 24.6.0: Mon Aug 11 21:15:09 PDT 2025; root:xnu-11417.140.69.701.11~1/RELEASE_ARM64_T6041 arm64
Subsystem
No response
What steps will reproduce the bug?
This code snippet will occasionally hang while waiting for the second drain event on MacOS, and will reliably hang while waiting for the first drain event on Linux:
const { createServer } = require('node:http');
function drainUncorked(target) {
return new Promise((resolve) => {
console.log('drain required:', target.writableNeedDrain);
target.once('drain', () => {
console.log('drain complete');
target.cork();
resolve();
});
target.uncork();
});
}
const s = createServer(async (req, res) => {
console.log('request');
res.cork();
console.log('1');
if (!res.write('1'.repeat(100))) await drainUncorked(res);
console.log('2');
if (!res.write('2'.repeat(1000000))) await drainUncorked(res);
console.log('3');
if (!res.write('3'.repeat(100))) await drainUncorked(res);
console.log('4');
if (!res.write('4'.repeat(1000000))) await drainUncorked(res);
console.log('5');
res.uncork();
res.end();
console.log('done');
});
s.listen(8080, 'localhost', async () => {
console.log('listening');
const res = await fetch('http://localhost:8080');
await res.text();
s.close();
});How often does it reproduce? Is there a required condition?
This reproduces intermittently on v24.10.0 on MacOS (tested: 3 failures in 30 attempts; ~10% failure rate), and reliably on v22.20.0 on Linux (specifically a Raspberry Pi running Linux pi5 6.12.47+rpt-rpi-v8 #1 SMP PREEMPT Debian 1:6.12.47-1+rpt1~bookworm (2025-09-16) aarch64 GNU/Linux)
What is the expected behavior? Why is that the expected behavior?
When writableNeedDrain is true (and equivalently if .write returns false), there should always be a drain event emitted once the stream has been consumed. This should apply regardless of whether the cork feature is being used (as long as the stream is uncorked once it needs to drain), and any listener registered while writableNeedDrain is true should be guaranteed to receive this drain event.
What do you see instead?
on macOS, the code above frequently hangs at this point:
listening request 1 2 drain required: true drain complete 3 4 drain required: true
By using curl -vvv localhost:8080 instead of the fetch example code, I can see that the response content is being drained successfully (i.e. the correct number of '4's are downloaded), so the drain event ought to be fired.
Testing by adding additional delays surprisingly makes this more likely to fail. For example, this adapted version of drainUncorked which waits for an event loop between each command fails every time on the second drain on macOS:
function drainUncorked(target) {
return new Promise((resolve) => {
console.log('drain required:', target.writableNeedDrain);
target.once('drain', async () => {
console.log('drain complete');
await new Promise((resolve) => setTimeout(resolve, 0));
target.cork();
await new Promise((resolve) => setTimeout(resolve, 0));
resolve();
});
target.uncork();
});
}Additional information
Without cork/uncork, the code always succeeds. I have also tested this on a raw socket and confirmed the drain event fires correctly there; this code succeeds every time:
# run netcat in the background as a destination for the socket
nc -l 8080const { Socket } = require('node:net');
function drainUncorked(target) {
return new Promise((resolve) => {
console.log('drain required:', target.writableNeedDrain);
target.once('drain', () => {
console.log('drain complete');
target.cork();
resolve();
});
target.uncork();
});
}
(async () => {
const s = new Socket();
s.connect(8080, 'localhost');
s.cork();
console.log('1');
if (!s.write('1'.repeat(100))) await drainUncorked(s);
console.log('2');
if (!s.write('2'.repeat(1000000))) await drainUncorked(s);
console.log('3');
if (!s.write('3'.repeat(100))) await drainUncorked(s);
console.log('4');
if (!s.write('4'.repeat(1000000))) await drainUncorked(s);
console.log('5');
s.uncork();
s.end();
})();Since this is unique to ServerResponse, I suspect it is related to the chunk merging behaviour from #50167 (and chunk merging is why I want to use cork in the first place)
Activity
davidje13 commentedon Oct 27, 2025
From adding the following additional logging, I can confirm that the specific issue is that the
drainevent is not firing (andwritableNeedDrainis not resetting tofalse) despite the stream draining:davidje13 commentedon Oct 27, 2025
A workaround for this issue is to use the callback of
writeinstead of listening fordrain:http: fix drain event with cork/uncork in ServerResponse
http: fix drain event with cork/uncork
github-actions commentedon May 26, 2026
This issue has been marked as stale due to 210 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.
davidje13 commentedon May 26, 2026
Looks like #60437 would have fixed this but tripped up on some test formatting and was closed / abandoned. @ThierryMT is it OK for me to pick up your changes and make the final tweaks requested by the maintainers? I see that your changes will still cleanly apply to the latest
main.http: fix drain event with cork/uncork
http: fix drain event with cork/uncork
http: fix drain event with cork/uncork
http: fix drain event with cork/uncork
http: fix drain event with cork/uncork
http: fix drain event with cork/uncork