Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 8 additions & 0 deletions src/js/node/_http_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,14 @@ function socketOnEnd() {
const req = this._httpMessage;
const parser = this.parser;

// An 'end' after req.destroy() is our own teardown, not a server FIN;
// finishing the parser here would mark an EOF-delimited body complete and
// let socketCloseListener deliver a clean 'end' for a locally aborted body.
if (req.destroyed) {
socket.destroy();
return;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

if (!req.res && !req.socket._hadError) {
// If we don't have a response then we know that the socket
// ended prematurely and we need to emit an error on the request.
Expand Down
47 changes: 47 additions & 0 deletions test/js/node/http/node-http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3898,6 +3898,53 @@ it("http.Agent with proxyEnv does not write to a literal 'undefined' property",
}
});

describe("req.destroy() mid-body reports the response as aborted", () => {
// RFC 9112 6.3: without Content-Length or Transfer-Encoding the body is
// framed by connection close. Destroying the request locally must surface as
// 'aborted' + ECONNRESET (the download is truncated), never a clean 'end'.
it.each([
["EOF-delimited", "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\npart1-", true],
["chunked", "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n6\r\npart1-\r\n", false],
])("%s", async (_name, responseBytes, expectedComplete) => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const server = createNetServer(s => {
s.on("error", () => {});
s.on("data", () => {
try {
s.write(responseBytes);
} catch {}
});
});
try {
server.listen(0, "127.0.0.1");
await once(server, "listening");
const { port } = server.address() as AddressInfo;

const { promise, resolve, reject } = Promise.withResolvers<{ events: string[]; complete: boolean }>();
const req = request({ host: "127.0.0.1", port }, res => {
const events: string[] = [];
res.on("data", () => {
events.push("data");
req.destroy();
});
res.on("end", () => events.push("end"));
res.on("aborted", () => events.push("aborted"));
res.on("error", (e: NodeJS.ErrnoException) => events.push(`error:${e.code}`));
res.on("close", () => resolve({ events, complete: res.complete }));
});
req.on("error", reject);
req.end();

const { events, complete } = await promise;
expect({ events, complete }).toEqual({
events: ["data", "aborted", "error:ECONNRESET"],
complete: expectedComplete,
});
} finally {
server.close();
}
});
});

it("OutgoingMessage outputData is per-instance and _flushOutput is defined", () => {
expect(typeof OutgoingMessage.prototype._flushOutput).toBe("function");

Expand Down
Loading