diff --git a/packages/bun-uws/src/HttpResponse.h b/packages/bun-uws/src/HttpResponse.h index 99618dd72b8..91469110c7a 100644 --- a/packages/bun-uws/src/HttpResponse.h +++ b/packages/bun-uws/src/HttpResponse.h @@ -154,6 +154,19 @@ struct HttpResponse : public AsyncSocket { } } else { this->uncork(); + /* After uncorking, check if we should close this connection. + * This handles the case where writeHead corked the socket outside + * the uWS request handler (async response), so the close check + * in HttpContext's onData handler never runs for this response. */ + if (httpResponseData->state & HttpResponseData::HTTP_CONNECTION_CLOSE) { + if ((httpResponseData->state & HttpResponseData::HTTP_RESPONSE_PENDING) == 0) { + if (((AsyncSocket *) this)->getBufferedAmount() == 0) { + ((AsyncSocket *) this)->shutdown(); + ((AsyncSocket *) this)->close(); + return true; + } + } + } } /* tryEnd can never fail when in chunked mode, since we do not have tryWrite (yet), only write */ @@ -219,6 +232,19 @@ struct HttpResponse : public AsyncSocket { } } else { this->uncork(); + /* After uncorking, check if we should close this connection. + * Same fix as the chunked path above. */ + if (httpResponseData->state & HttpResponseData::HTTP_CONNECTION_CLOSE) { + if ((httpResponseData->state & HttpResponseData::HTTP_RESPONSE_PENDING) == 0) { + if (((AsyncSocket *) this)->getBufferedAmount() == 0) { + ((AsyncSocket *) this)->shutdown(); + ((AsyncSocket *) this)->close(); + /* Return immediately after close to prevent + * use-after-free on the freed socket. */ + return true; + } + } + } } } diff --git a/test/js/node/http/node-http-async-chunked-close.test.ts b/test/js/node/http/node-http-async-chunked-close.test.ts new file mode 100644 index 00000000000..5d38a171729 --- /dev/null +++ b/test/js/node/http/node-http-async-chunked-close.test.ts @@ -0,0 +1,48 @@ +import { test, expect } from "bun:test"; +import * as http from "node:http"; +import * as net from "node:net"; + +test("http.Server closes connection after async chunked response with Connection: close", async () => { + // Server that responds asynchronously with chunked encoding + const server = http.createServer((req, res) => { + setTimeout(() => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.write("chunk1"); + res.write("chunk2"); + res.write("chunk3"); + res.end(); + }, 10); + }); + + await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); + const port = (server.address() as net.AddressInfo).port; + + try { + const result = await new Promise<{ pass: boolean; body: string }>(resolve => { + const socket = net.createConnection(port, "127.0.0.1", () => { + socket.write("GET / HTTP/1.1\r\nHost: test\r\nConnection: close\r\n\r\n"); + }); + + const chunks: Buffer[] = []; + socket.on("data", c => chunks.push(c)); + socket.on("error", err => { + resolve({ pass: false, body: `Connection error: ${err.message}` }); + socket.destroy(); + }); + socket.on("end", () => { + resolve({ pass: true, body: Buffer.concat(chunks).toString() }); + }); + socket.setTimeout(3000, () => { + resolve({ pass: false, body: Buffer.concat(chunks).toString() }); + socket.destroy(); + }); + }); + + expect(result.body).toContain("chunk1"); + expect(result.body).toContain("chunk2"); + expect(result.body).toContain("chunk3"); + expect(result.pass).toBe(true); + } finally { + server.close(); + } +});