Skip to content

Commit 6e4fcf0

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 (socket[kCloseCallback] = ...) instead of registering a real EventEmitter listener. Nothing was invoking that stored callback when the socket's underlying handle closed, so ServerResponse.on('close', ...) never ran on client abort. Fire callCloseCallback(this) at the end of NodeHTTPServerSocket#onClose after the request destroy, mirroring what Node's socket.on('close') listener does. This covers both the mid-response case (#29219) and the no-write case (#14697). Fixes #29219 Fixes #14697
1 parent 7c75a87 commit 6e4fcf0

File tree

2 files changed

+92
-0
lines changed

2 files changed

+92
-0
lines changed

src/js/node/_http_server.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,12 @@ const NodeHTTPServerSocket = class Socket extends Duplex {
908908
req.destroy();
909909
}
910910
}
911+
912+
// Fire the close callback set by assignSocketInternal
913+
// (onServerResponseClose). This forwards the socket close to the
914+
// attached ServerResponse so `res.on("close", ...)` runs on client
915+
// disconnect, matching Node.js. See issues #29219, #14697.
916+
callCloseCallback(this);
911917
}
912918
#onCloseForDestroy(closeCallback) {
913919
this.#onClose();
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// https://github.com/oven-sh/bun/issues/29219
2+
import { expect, test } from "bun:test";
3+
import http from "node:http";
4+
import net from "node:net";
5+
6+
test("ServerResponse emits 'close' when the client aborts mid-response", async () => {
7+
const { promise, resolve } = Promise.withResolvers<{
8+
resClosed: boolean;
9+
reqErrored: boolean;
10+
}>();
11+
12+
let resClosed = false;
13+
let reqErrored = false;
14+
15+
const server = http.createServer((req, res) => {
16+
res.on("close", () => {
17+
resClosed = true;
18+
});
19+
20+
// Write some data so the response is mid-stream when the client aborts.
21+
res.write("hello\n");
22+
23+
req.on("error", () => {
24+
reqErrored = true;
25+
server.close(() => {
26+
resolve({ resClosed, reqErrored });
27+
});
28+
});
29+
});
30+
31+
await new Promise<void>(done => {
32+
server.listen(0, "127.0.0.1", () => {
33+
const { port } = server.address() as net.AddressInfo;
34+
const client = net.createConnection({ port, host: "127.0.0.1" }, () => {
35+
client.write("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n");
36+
client.on("data", () => {
37+
client.end(); // force abort mid-response
38+
});
39+
});
40+
done();
41+
});
42+
});
43+
44+
const result = await promise;
45+
expect(result).toEqual({ resClosed: true, reqErrored: true });
46+
});
47+
48+
// Also covers https://github.com/oven-sh/bun/issues/14697 — same root
49+
// cause, but the handler never writes a response before the client
50+
// disconnects. Pre-fix, only `req.on("close")` fired.
51+
test("ServerResponse emits 'close' when the client aborts before any write", async () => {
52+
const { promise, resolve } = Promise.withResolvers<{
53+
resClosed: boolean;
54+
reqClosed: boolean;
55+
}>();
56+
57+
let resClosed = false;
58+
let reqClosed = false;
59+
60+
const server = http.createServer((req, res) => {
61+
res.once("close", () => {
62+
resClosed = true;
63+
if (reqClosed) server.close(() => resolve({ resClosed, reqClosed }));
64+
});
65+
req.once("close", () => {
66+
reqClosed = true;
67+
if (resClosed) server.close(() => resolve({ resClosed, reqClosed }));
68+
});
69+
// Deliberately don't write or end — wait for the client to go away.
70+
});
71+
72+
await new Promise<void>(done => {
73+
server.listen(0, "127.0.0.1", () => {
74+
const { port } = server.address() as net.AddressInfo;
75+
const client = net.createConnection({ port, host: "127.0.0.1" }, () => {
76+
client.write("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n");
77+
// Wait a tick so the server receives the request, then yank the socket.
78+
setTimeout(() => client.destroy(), 100);
79+
});
80+
done();
81+
});
82+
});
83+
84+
const result = await promise;
85+
expect(result).toEqual({ resClosed: true, reqClosed: true });
86+
});

0 commit comments

Comments
 (0)