Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/bun-uws/src/HttpResponse.h
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,19 @@ struct HttpResponse : public AsyncSocket<SSL> {
}
} 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<SSL>::HTTP_CONNECTION_CLOSE) {
if ((httpResponseData->state & HttpResponseData<SSL>::HTTP_RESPONSE_PENDING) == 0) {
if (((AsyncSocket<SSL> *) this)->getBufferedAmount() == 0) {
((AsyncSocket<SSL> *) this)->shutdown();
((AsyncSocket<SSL> *) this)->close();
return true;
}
}
}
}

/* tryEnd can never fail when in chunked mode, since we do not have tryWrite (yet), only write */
Expand Down Expand Up @@ -219,6 +232,19 @@ struct HttpResponse : public AsyncSocket<SSL> {
}
} else {
this->uncork();
/* After uncorking, check if we should close this connection.
* Same fix as the chunked path above. */
if (httpResponseData->state & HttpResponseData<SSL>::HTTP_CONNECTION_CLOSE) {
if ((httpResponseData->state & HttpResponseData<SSL>::HTTP_RESPONSE_PENDING) == 0) {
if (((AsyncSocket<SSL> *) this)->getBufferedAmount() == 0) {
((AsyncSocket<SSL> *) this)->shutdown();
((AsyncSocket<SSL> *) this)->close();
/* Return immediately after close to prevent
* use-after-free on the freed socket. */
return true;
}
}
}
}
}

Expand Down
48 changes: 48 additions & 0 deletions test/js/node/http/node-http-async-chunked-close.test.ts
Original file line number Diff line number Diff line change
@@ -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<void>(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();
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

expect(result.body).toContain("chunk1");
expect(result.body).toContain("chunk2");
expect(result.body).toContain("chunk3");
expect(result.pass).toBe(true);
} finally {
server.close();
}
});