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
2 changes: 1 addition & 1 deletion src/middleware/web-incoming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const stream = defineProxyMiddleware((req, res, options, server, head, ca

function createErrorHandler(proxyReq: ClientRequest, url: URL | ProxyTargetDetailed) {
return function proxyError(err: Error) {
if (req.socket?.destroyed && (err as NodeJS.ErrnoException).code === "ECONNRESET") {
if (!req.socket?.writable && (err as NodeJS.ErrnoException).code === "ECONNRESET") {
server.emit("econnreset", err, req, res, url);
return proxyReq.destroy();
}
Expand Down
67 changes: 67 additions & 0 deletions test/http-proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,73 @@ describe("http-proxy", () => {
});
});

describe("#createProxyServer client disconnect", () => {
it("should emit econnreset instead of error when client disconnects", async () => {
const { promise, resolve, reject } = Promise.withResolvers<void>();

// Target server that accepts connections but never responds
const source = net.createServer();
const sourcePort = await listenOn(source);

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

// Intercept proxyReq to simulate the race condition where the client
// has disconnected (socket is no longer writable) but req.socket.destroyed
// is still false — reproducing the timing issue from upstream PR #1542.
proxy.on("proxyReq", (proxyReq, req) => {
setTimeout(() => {
const socket = req.socket;
Object.defineProperty(socket, "writable", { value: false, configurable: true });
Object.defineProperty(socket, "destroyed", { value: false, configurable: true });
proxyReq.emit(
"error",
Object.assign(new Error("read ECONNRESET"), { code: "ECONNRESET" }),
);
}, 50);
});

const proxyServer = http.createServer((req, res) => {
proxy.web(req, res);
});
const proxyPort = await listenOn(proxyServer);

proxy.on("econnreset", (err) => {
expect(err).toBeInstanceOf(Error);
expect((err as any).code).toBe("ECONNRESET");
proxy.close(() => {});
proxyServer.close();
source.close();
resolve();
});

proxy.on("error", (err) => {
proxy.close(() => {});
proxyServer.close();
source.close();
reject(new Error(`Unexpected error event: ${(err as any).code || err.message}`));
});

const testReq = http.request(
{
hostname: "127.0.0.1",
port: proxyPort,
method: "GET",
},
() => {},
);

testReq.on("error", () => {
// Expected
});

testReq.end();

await promise;
});
});

describe("#createProxyServer with xfwd option", () => {
it("should not throw on empty http host header", async () => {
const source = http.createServer();
Expand Down
Loading