Repository navigation
Description
When a peer sends a response and immediately resets the TCP connection while the local endpoint still has pending writes, the kqueue backend can dispatch the write-side error before pending readable data.
On affected macOS runs, this causes uv__write() to report EPIPE before Node.js can parse an already-sent HTTP response. The write error then marks the Duplex readable side as errored, so the response is never delivered to the HTTP parser.
This issue concerns the Darwin/kqueue ordering found while investigating the Node.js HTTP regression. It may not be the sole cause of the original Linux release boundary between Node.js v24.15 and v24.16.
code example
'use strict';
const http = require('node:http');
const body = Buffer.alloc(2 * 1024 * 1024);
const server = http.createServer((request, response) => {
let received = 0;
request.on('data', function onData(chunk) {
received += chunk.length;
if (received <= body.length / 2) return;
request.off('data', onData);
response.writeHead(413, { 'content-length': 0 });
response.end();
request.destroy();
});
});
server.listen(0, () => {
const req = http.request({
method: 'POST',
port: server.address().port,
headers: { 'content-length': body.length },
}, (res) => {
console.log('response', res.statusCode);
res.resume();
res.on('end', () => console.log('response complete', res.complete));
});
req.on('error', (err) => {
console.log({
code: err.code,
syscall: err.syscall,
writableFinished: req.writableFinished,
hasResponse: Boolean(req.res),
responseComplete: req.res?.complete,
});
});
req.on('close', () => server.close());
req.end(body);
});On affected Darwin runs, the first observable result is similar to:
code: EPIPE
syscall: write
writableFinished: true
hasResponse: false
A patched kqueue backend showed an EVFILT_WRITE event with EV_EOF and a non-zero fflags socket error being delivered before the corresponding readable event.
The current kqueue mapping (https://github.com/libuv/libuv/blob/v1.52.0/src/unix/kqueue.c#L330-L400) maps that event to POLLOUT. It maps EV_ERROR to POLLERR, but does not account for a runtime socket error carried by EV_EOF and fflags.
Consequently, uv__stream_io() (https://github.com/libuv/libuv/blob/v1.52.0/src/unix/stream.c#L1190-L1238) receives only POLLOUT and calls uv__write() first. The write produces EPIPE.
If the write error is temporarily prevented from disabling the readable side, the same socket subsequently delivers a complete HTTP response. This indicates that response data was still available but had not reached the HTTP parser when the write error was propagated.
Darwin stores the reset error in so_error, closes the TCP state, and wakes both socket directions. Since kqueue represents read and write filters as separate events, their delivery order can differ from epoll, where readiness and error flags for the same descriptor are commonly returned together.
Diagnostic runs:
- Raw kqueue instrumentation (https://github.com/Archkon/node/actions/runs/29524179884/job/87708326818)
- Delayed readable-side error propagation (https://github.com/Archkon/node/actions/runs/29488284966/job/87588214115)
Possible solution ?
One possible backend normalization is:
if (ev->filter == EVFILT_WRITE &&
(ev->flags & EV_EOF) &&
ev->fflags != 0)
revents |= POLLERR;This retains POLLOUT and adds POLLERR. The existing stream handler would therefore attempt uv__read() before uv__write().
Is this normalization appropriate for libuv’s kqueue backend, or is the current platform-dependent ordering considered expected behavior?
Activity
Archkon commentedon Jul 17, 2026
And could check this nodejs/node#64507 (comment)
vtjnash commentedon Jul 17, 2026
That's an apparent violation of the TCP standard (and HTTP standard), so this seems to be working as intended in the presence of such a concurrency mistake.
I don't want to research and explain the precise violation, so I am letting Claude do it:
Archkon commentedon Jul 17, 2026
@vtjnash
Thank you for pointing out this behavior, but all rfc document from claude seems just say the response would be not definite to be read by clinet due to kernel buffer clear or drop or lost in transport but I mean that when the client could truly read this data packet and proccessing http parse while kqueue reports EVFILT_WRITE + EV_EOF with a socket error , should libuv normalize the event to include POLLERR, allowing currently readable data to be cotinusouly consumed before the write error is exposed ?
Archkon commentedon Jul 17, 2026
@vtjnash Apologies for pinging you again. Would you be willing to reopen this issue and discuss my proposal with the other libuv maintainers? I realize this may be asking a lot, and I sincerely apologize for the imposition.
I just meant to solve this case "only helps in the case where the response bytes happen to already be buffered locally when the error is delivered"
vtjnash commentedon Jul 17, 2026
No, libuv shouldn't try to adapt to clients that deliberately violate the TCP standard. That way just lies madness (and ever more difficult to diagnose bugs--like the infamous 500 mile email).
vtjnash commentedon Jul 17, 2026
You've mentioned the data is definitely in the kernel buffer, but that's what is not safe to assume here. The TCP state diagram shows the data was enroute to that buffer, but the RFC protocol instructs any intermediate handlers to discard that data upon observation of the RST packet. It seems that in your limited testing, that did happen to be already in the kernel buffer, but that makes a lot of timing assumptions that aren't valid for the TCP standard (as detailed in RFC 9112 §9.6).
Archkon commentedon Jul 17, 2026
@vtjnash
Sorry,but I don't assume that "data is definitely in the kernel buffer" ,I just assume "if".I don't know whether there would be some wording problem mislead you ?And RFC 9112 §9.6 just said "the reset packet might erase the client's unacknowledged input buffers before they can be read and interpreted by the client's HTTP parser" so not definitely drop data packet in buffer ?I don't know if I miss something?
Archkon commentedon Jul 17, 2026
@vtjnash
And I don't mean that let libuv to adapt to tcp client that violate tcp standard
I just to say that as the reference from rfc document, it seems that data packet don't definitely be drop by kernel while write error happend, rfc document seems never said we should't let the data packet be read if that is readable for this conditon?I think if we could let the readable data to be read which not violate what rfc document said ?
I don't know whether my understanding is correct ? I'm happy that you could guide me something I may miss? I
http: defer write errors for pending responses
http: defer write errors for pending responses