Follow a redirect whose chunked body has not ended (#2484)

This commit is contained in:
Chris Tate
2026-10-28 16:06:56 -05:00
committed by GitHub
4 changed files with 63 additions and 1 deletions
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/http-client-response-aborted-unlistened.ts"
],
"diags": []
}
+3 -1
View File
@@ -2376,7 +2376,9 @@ static void scr_http_proto_sweep(void) {
}
case SCR_HTTP_EMIT_REQ_ABORTED: {
ScrHttpReq *req = (ScrHttpReq *)e.h;
if (!req->close_emitted) {
/* Node's IncomingMessage reports its destroy error only to 'error'
* listeners: without one, 'aborted' and 'close' alone report it. */
if (!req->close_emitted && req->err_ls.n != 0) {
ScrStr *msg = scr_str_new("aborted", 7);
scr_net_fire_err_this(&req->err_ls, msg, req, SCR_DYNH_HTTP_REQ);
scr_str_release(msg);
@@ -0,0 +1,53 @@
// A client response cut off mid-body reports 'aborted' and 'close'; its
// 'error' reaches 'error' listeners only, so a response without one does not
// end the process. fetch relies on this when it leaves a redirect whose
// chunked body has not ended.
import * as http from "node:http";
import * as net from "node:net";
const server = net.createServer((socket) => {
let buffered = "";
socket.on("data", (chunk) => {
buffered += chunk.toString("latin1");
while (buffered.includes("\r\n\r\n")) {
const end = buffered.indexOf("\r\n\r\n");
const path = buffered.slice(0, end).split(" ")[1];
buffered = buffered.slice(end + 4);
if (path === "/final") {
socket.write("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nfinal");
continue;
}
socket.write("HTTP/1.1 303 See Other\r\nLocation: /final\r\nTransfer-Encoding: chunked\r\n\r\n");
// The terminating chunk arrives in a later read.
setTimeout(() => {
if (!socket.destroyed) socket.write("0\r\n\r\n");
}, 50);
}
});
socket.on("error", () => {});
});
function destroyMidBody(port: number, listen: boolean): Promise<void> {
return new Promise((resolve) => {
const req = http.get({ host: "127.0.0.1", port, path: "/start" }, (res) => {
console.log("status", res.statusCode);
res.on("aborted", () => console.log("aborted"));
if (listen) res.on("error", (error: NodeJS.ErrnoException) => console.log("res error", error.message, error.code));
res.on("close", () => {
console.log("res close");
resolve();
});
req.destroy();
});
req.on("close", () => console.log("req close"));
});
}
server.listen(0, "127.0.0.1", async () => {
const port = (server.address() as net.AddressInfo).port;
const response = await fetch(`http://127.0.0.1:${port}/start`);
console.log(response.status, response.redirected, response.url.endsWith("/final"), await response.text());
await destroyMidBody(port, false);
await destroyMidBody(port, true);
server.close();
});
@@ -0,0 +1 @@
{ "plain": 15300, "san": 69300 }