Skip to content

Commit 9044d39

Browse files
committed
http: emit ServerResponse 'close' on client disconnect
When the canUseInternalAssignSocket fast path is taken, assignSocketInternal stores onServerResponseClose on the socket via setCloseCallback (self[kCloseCallback] = ...) instead of registering a real EventEmitter listener. The 'close' event the Duplex fires when the underlying handle goes away never triggered that stored callback, so ServerResponse.on('close', ...) never fired on client abort. Hook NodeHTTPServerSocket.emit so the stored callback runs when the socket emits 'close', mirroring what ServerResponse.prototype.emit already does for the response side. Fixes #29219
1 parent 7c75a87 commit 9044d39

File tree

2 files changed

+63
-0
lines changed

2 files changed

+63
-0
lines changed

src/js/node/_http_server.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,18 @@ const NodeHTTPServerSocket = class Socket extends Duplex {
11061106
return super.resume();
11071107
}
11081108

1109+
emit(event, ...args) {
1110+
if (event === "close") {
1111+
// Trigger the stored close callback (onServerResponseClose from
1112+
// assignSocketInternal) so the attached ServerResponse receives its
1113+
// own 'close' event. The canUseInternalAssignSocket path stores the
1114+
// callback as self[kCloseCallback] instead of adding a real event
1115+
// listener, so we need this hook to reach it. See issue #29219.
1116+
callCloseCallback(this);
1117+
}
1118+
return super.emit(event, ...args);
1119+
}
1120+
11091121
get [kInternalSocketData]() {
11101122
return this[kHandle]?.response;
11111123
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// https://github.com/oven-sh/bun/issues/29219
2+
//
3+
// http.ServerResponse must emit 'close' when the client disconnects in the
4+
// middle of an in-flight response. Pre-fix, `res.on('close')` never fired
5+
// because the stored `onServerResponseClose` callback on the socket was
6+
// never invoked when the socket emitted its own 'close' event.
7+
import { expect, test } from "bun:test";
8+
import http from "node:http";
9+
import net from "node:net";
10+
11+
test("ServerResponse emits 'close' when the client aborts mid-response", async () => {
12+
const { promise, resolve } = Promise.withResolvers<{
13+
resClosed: boolean;
14+
reqErrored: boolean;
15+
}>();
16+
17+
let resClosed = false;
18+
let reqErrored = false;
19+
20+
const server = http.createServer((req, res) => {
21+
res.on("close", () => {
22+
resClosed = true;
23+
});
24+
25+
// Write some data so the response is mid-stream when the client aborts.
26+
res.write("hello\n");
27+
28+
req.on("error", () => {
29+
reqErrored = true;
30+
server.close(() => {
31+
resolve({ resClosed, reqErrored });
32+
});
33+
});
34+
});
35+
36+
await new Promise<void>(done => {
37+
server.listen(0, "127.0.0.1", () => {
38+
const { port } = server.address() as net.AddressInfo;
39+
const client = net.createConnection({ port, host: "127.0.0.1" }, () => {
40+
client.write("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n");
41+
client.on("data", () => {
42+
client.end(); // force abort mid-response
43+
});
44+
});
45+
done();
46+
});
47+
});
48+
49+
const result = await promise;
50+
expect(result).toEqual({ resClosed: true, reqErrored: true });
51+
});

0 commit comments

Comments
 (0)