Skip to content
Merged
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
11 changes: 3 additions & 8 deletions src/middleware/web-incoming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,10 @@ export const stream = defineProxyMiddleware((req, res, options, server, head, ca
// show an error page at the initial request
if (options.proxyTimeout) {
proxyReq.setTimeout(options.proxyTimeout, function () {
proxyReq.abort();
proxyReq.destroy();
});
}

// Ensure we abort proxy if request is aborted
req.on("aborted", function () {
proxyReq.abort();
});

// Abort proxy request when client disconnects
res.on("close", function () {
if (!res.writableFinished) {
Expand All @@ -136,7 +131,7 @@ export const stream = defineProxyMiddleware((req, res, options, server, head, ca
return function proxyError(err: Error) {
if (req.socket.destroyed && (err as NodeJS.ErrnoException).code === "ECONNRESET") {
server.emit("econnreset", err, req, res, url);
return proxyReq.abort();
return proxyReq.destroy();
}

if (callback) {
Expand Down Expand Up @@ -230,7 +225,7 @@ export const stream = defineProxyMiddleware((req, res, options, server, head, ca

if (options.proxyTimeout) {
redirectReq.setTimeout(options.proxyTimeout, () => {
redirectReq.abort();
redirectReq.destroy();
});
}

Expand Down
56 changes: 56 additions & 0 deletions test/http-proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,62 @@ describe("http-proxy", () => {
expect(gotFirstChunk).toBe(true);
});

it("should destroy upstream proxy request when client aborts", async () => {
const { promise, resolve, reject } = Promise.withResolvers<void>();

// Track whether the upstream request was properly destroyed
let upstreamReqDestroyed = false;

const source = http.createServer((req, res) => {
// SSE-like long-lived response
res.writeHead(200, {
"content-type": "text/event-stream",
"cache-control": "no-cache",
connection: "keep-alive",
});
res.write(":ok\n\n");

req.socket.on("close", () => {
upstreamReqDestroyed = true;
});
});
const sourcePort = await listenOn(source);

const proxy = httpProxy.createProxyServer({
target: "http://127.0.0.1:" + sourcePort,
});
const proxyPort = await proxyListen(proxy);

const timeout = setTimeout(() => {
reject(new Error("Timed out: upstream request was not destroyed after client abort"));
}, 2000);

// Make a request and abort it after receiving the first chunk
const clientReq = http.request(
{ hostname: "127.0.0.1", port: proxyPort, method: "GET" },
(res) => {
res.once("data", () => {
// Client received data, now abort the connection
clientReq.destroy();
});
},
);
clientReq.end();

// Poll for upstream destruction
const check = setInterval(() => {
if (upstreamReqDestroyed) {
clearInterval(check);
clearTimeout(timeout);
source.close();
proxy.close(() => resolve());
}
}, 20);

await promise;
expect(upstreamReqDestroyed).toBe(true);
});

it("should make the request on pipe and finish it", async () => {
const source = http.createServer();
const sourcePort = await listenOn(source);
Expand Down
Loading