Description
- Version: 14.0.0...14.15.3 (latest lts), 15.0.0...15.5.0 (latest stable)
- Platform: macos / ubuntu / windows
- Subsystem: N/A
What steps will reproduce the bug?
Originally, I noticed this behaviour in a Next.js app and it took me quite a while to drill down to the bottom of it. You can find a plain MWE here: https://github.com/kachkaev/node-http-response-double-end-call-breaking-drain-event (no NPM packages involved).
Since Node version 14.0.0, calling res.end()
twice in a body-less response seems to be silencing the drain
event in a follow-up HTTP request if it uses the same connection. This can happen in practice when redirecting a client to a heavy page and using compression package as middleware.
I understand that calling res.end()
twice is a developer mistake, however it does not seem right to have to debug such a small oversight for more than two working days 😅 When using res.redirect(...)
helper method in Next.js, it’s easy to forget that it’s not only doing res.writeHead(...)
for you, but also calls res.end()
. Seeing res.redirect(...); res.end()
does not feel too wrong initially and there is no feedback from the server or the tooling to suggest that this involves res.end()
being called twice.
Here are the reproduction steps from the server POV:
-
A client establishes a connection and requests a page that results with a redirect:
res.writeHead(302, { Location: "/another-page" }); res.end(); res.end(); // called twice intentionally
-
The same client immediately comes back with another request, which is meant to return 200 and contain some payload.
-
If the size of the payload is small enough to fit a single
res.write(...)
, all works fine. -
If the payload involves
res.write(...) === true
→res.on("drain", () => {...})
, the second request is never finished because thedrain
event is never invoked.
-
You can look into how compression
is using res.write(...)
+ res.on("drain", ...)
to find a practical example: https://github.com/expressjs/compression/blob/3fea81d0eaed1eb872bf3e0405f20d9e175ab2cf/index.js#L193-L218
How often does it reproduce? Is there a required condition?
I am able to silence the drain
event quite reliably on any Node version ≥14.0.0 on any OS. See the GitHub Workflow for my MWE.
What is the expected behavior?
Ideally, I would expect the second res.end()
to not produce any side effects or at least to give me a warning. All works fine in Node v13.14.0 and below.
What do you see instead?
I stumbled across some magic behaviour which took more than two days to investigate 😅
Additional information
The unwanted side effect from a double call to res.end()
is negated:
- if the follow-up request does not share the connection with the first (body-less) request (i.e.
curl
called twice); - or if
res.write("")
is added to the first (body-less) request
Both observations are included into GitHub Workflows within the MWE repo. I also tried playing with res.end("")
as a replacement for res.write("")
, but it did not help.