Skip to content

Commit c9f0023

Browse files
author
vhess
committed
update error handling in stream2, add abort controller
1 parent 59aed83 commit c9f0023

File tree

3 files changed

+110
-2
lines changed

3 files changed

+110
-2
lines changed

lib/http-proxy/passes/web-incoming.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export function XHeaders(req: Request, _res: Response, options: ServerOptions) {
6969
(req.headers["x-forwarded-" + header] || "") + (req.headers["x-forwarded-" + header] ? "," : "") + values[header];
7070
}
7171

72-
req.headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || req.headers["host"] || "";
72+
req.headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || req.headers["host"] || req.headers[":authority"] || "";
7373
}
7474

7575
// Does the actual proxying. If `forward` is enabled fires up
@@ -208,6 +208,12 @@ async function stream2(
208208
) {
209209
// Helper function to handle errors consistently throughout the fetch path
210210
const handleError = (err: Error, target?: ProxyTargetUrl) => {
211+
const e = err as any;
212+
// Copy code from cause if available and missing on err
213+
if (e.code === undefined && e.cause?.code) {
214+
e.code = e.cause.code;
215+
}
216+
211217
if (cb) {
212218
cb(err, req, res, target);
213219
} else {
@@ -229,9 +235,27 @@ async function stream2(
229235
const customFetch = options.fetch || fetch;
230236
const fetchOptions = options.fetchOptions ?? {} as FetchOptions;
231237

238+
const controller = new AbortController();
239+
const { signal } = controller;
240+
241+
if (options.proxyTimeout) {
242+
setTimeout(() => {
243+
controller.abort();
244+
}, options.proxyTimeout);
245+
}
246+
247+
// Ensure we abort proxy if request is aborted
248+
res.on("close", () => {
249+
const aborted = !res.writableFinished;
250+
if (aborted) {
251+
controller.abort();
252+
}
253+
});
254+
232255
const prepareRequest = (outgoing: common.Outgoing) => {
233256
const requestOptions: RequestInit = {
234257
method: outgoing.method,
258+
signal,
235259
...fetchOptions.requestOptions,
236260
};
237261

@@ -294,6 +318,16 @@ async function stream2(
294318
}
295319
}
296320
} catch (err) {
321+
if ((err as Error).name === "AbortError") {
322+
// Handle aborts (timeout or client disconnect)
323+
if (options.proxyTimeout && signal.aborted) {
324+
const proxyTimeoutErr = new Error("Proxy timeout");
325+
(proxyTimeoutErr as any).code = "ECONNRESET";
326+
handleError(proxyTimeoutErr, options.forward);
327+
}
328+
// If aborted by client (res.close), we might not want to emit an error or maybe just log it
329+
return;
330+
}
297331
handleError(err as Error, options.forward);
298332
}
299333

@@ -385,6 +419,14 @@ async function stream2(
385419
server?.emit("end", req, res, fakeProxyRes);
386420
}
387421
} catch (err) {
422+
if ((err as Error).name === "AbortError") {
423+
if (options.proxyTimeout && signal.aborted) {
424+
const proxyTimeoutErr = new Error("Proxy timeout");
425+
(proxyTimeoutErr as any).code = "ECONNRESET";
426+
handleError(proxyTimeoutErr, options.target);
427+
}
428+
return;
429+
}
388430
handleError(err as Error, options.target);
389431
}
390432
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
2+
import * as http from "node:http";
3+
import * as httpProxy from "../..";
4+
import getPort from "../get-port";
5+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
6+
import { fetch } from "undici";
7+
8+
describe("Fetch Proxy Timeout", () => {
9+
let ports: Record<"http" | "proxy", number>;
10+
beforeAll(async () => {
11+
ports = { http: await getPort(), proxy: await getPort() };
12+
});
13+
14+
const servers: Record<string, any> = {};
15+
16+
it("Create the target HTTP server that hangs", async () => {
17+
servers.http = http
18+
.createServer((_req, _res) => {
19+
// Do nothing, let it hang
20+
})
21+
.listen(ports.http);
22+
});
23+
24+
it("Create the proxy server with fetch and timeout", async () => {
25+
servers.proxy = httpProxy
26+
.createServer({
27+
target: `http://localhost:${ports.http}`,
28+
fetch: fetch, // Enable fetch path
29+
proxyTimeout: 500, // 500ms timeout
30+
})
31+
.listen(ports.proxy);
32+
});
33+
34+
it("should timeout the request and emit error", async () => {
35+
return new Promise<void>((resolve, reject) => {
36+
const timeout = setTimeout(() => {
37+
reject(new Error("Test timed out"));
38+
}, 2000);
39+
40+
servers.proxy.once('error', (err: Error, _req: any, res: any) => {
41+
clearTimeout(timeout);
42+
try {
43+
expect(err).toBeTruthy();
44+
expect(err.message).toBe("Proxy timeout");
45+
res.statusCode = 504;
46+
res.end("Gateway Timeout");
47+
resolve();
48+
} catch (e) {
49+
reject(e);
50+
}
51+
});
52+
53+
fetch(`http://localhost:${ports.proxy}`).catch(() => {
54+
// Ignore client side fetch error, we care about server side error emission
55+
});
56+
});
57+
});
58+
59+
afterAll(async () => {
60+
Object.values(servers).map((x: any) => x?.close());
61+
});
62+
});

lib/test/lib/http-proxy-passes-web-incoming.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ describe("#createProxyServer.web() using own http server", () => {
119119
const source = http.createServer((req, res) => {
120120
res.end();
121121
expect(req.method).toEqual("GET");
122-
expect(req.headers.host?.split(":")[1]).toEqual(`${ports["8081"]}`);
122+
if (process.env.FORCE_FETCH_PATH === "true") {
123+
expect(req.headers.host?.split(":")[1]).toEqual(`${ports["8080"]}`);
124+
} else {
125+
expect(req.headers.host?.split(":")[1]).toEqual(`${ports["8081"]}`);
126+
}
123127
});
124128

125129
proxyServer.listen(ports["8081"]);

0 commit comments

Comments
 (0)