From a13d600865dba69317aa2d8ac1ff2908e858060b Mon Sep 17 00:00:00 2001 From: vhess Date: Tue, 18 Nov 2025 16:40:00 +0100 Subject: [PATCH 1/4] Add custom Fetch option and supersede dispatcher option --- README.md | 33 +++++----- lib/http-proxy/index.ts | 12 +--- lib/http-proxy/passes/web-incoming.ts | 21 +++---- lib/test/http/customFetch.test.ts | 71 ++++++++++++++++++++++ lib/test/http/proxy-callbacks.test.ts | 9 +-- lib/test/http/proxy-http2-to-http2.test.ts | 2 +- 6 files changed, 102 insertions(+), 46 deletions(-) create mode 100644 lib/test/http/customFetch.test.ts diff --git a/README.md b/README.md index 4276a2d..f14c208 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ import { Agent } from "undici"; const proxy = createProxyServer({ target: "https://127.0.0.1:5050", fetch: { - dispatcher: new Agent({ allowH2: true }), + requestOptions: {dispatcher: new Agent({ allowH2: true })}, // Modify the request before it's sent onBeforeRequest: async (requestOptions, req, res, options) => { requestOptions.headers['X-Special-Proxy-Header'] = 'foobar'; @@ -460,8 +460,8 @@ setGlobalDispatcher(new Agent({ allowH2: true })); // Or create a proxy with HTTP/2 support using fetch const proxy = createProxyServer({ target: "https://http2-server.example.com", - fetch: { - dispatcher: new Agent({ allowH2: true }) + fetchOptions: { + requestOptions: {dispatcher: new Agent({ allowH2: true })} } }); ``` @@ -481,17 +481,17 @@ const proxy = createProxyServer({ ```js const proxy = createProxyServer({ target: "https://api.example.com", - fetch: { - // Use undici's Agent for HTTP/2 support - dispatcher: new Agent({ - allowH2: true, - connect: { - rejectUnauthorized: false, // For self-signed certs - timeout: 10000 - } - }), + fetchOptions: { + requestOptions: { + // Use undici's Agent for HTTP/2 support + dispatcher: new Agent({ + allowH2: true, + connect: { + rejectUnauthorized: false, // For self-signed certs + timeout: 10000 + } + }), // Additional fetch request options - requestOptions: { headersTimeout: 30000, bodyTimeout: 60000 }, @@ -639,9 +639,8 @@ const proxy = createProxyServer({ - **ca**: Optionally override the trusted CA certificates. This is passed to https.request. -- **fetch**: Enable fetch API for HTTP/2 support. Set to `true` for defaults, or provide custom configuration: - - `dispatcher`: Custom fetch dispatcher (e.g., undici Agent with `allowH2: true` for HTTP/2) - - `requestOptions`: Additional fetch request options +- **fetchOptions**: Enable fetch API for HTTP/2 support. Set to `true` for defaults, or provide custom configuration: + - `requestOptions`: Additional fetch request options (e.g., undici Agent with `allowH2: true` for HTTP/2 as dispatcher) - `onBeforeRequest`: Async callback called before making the fetch request - `onAfterResponse`: Async callback called after receiving the fetch response @@ -696,7 +695,7 @@ The following table shows which configuration options are compatible with differ **Code Path Selection:** - **Native Path**: Used by default, supports HTTP/1.1 and WebSockets -- **Fetch Path**: Activated when `fetch` option is provided, supports HTTP/2 (with appropriate dispatcher) +- **Fetch Path**: Activated when `fetchOptions` option is provided, supports HTTP/2 (with appropriate dispatcher) **Event Compatibility:** - **Native Path**: Emits traditional events (`proxyReq`, `proxyRes`, `proxyReqWs`) diff --git a/lib/http-proxy/index.ts b/lib/http-proxy/index.ts index 7a6d08c..9d554e3 100644 --- a/lib/http-proxy/index.ts +++ b/lib/http-proxy/index.ts @@ -98,18 +98,10 @@ export interface ServerOptions { */ ca?: string; /** Enable using fetch for proxy requests. Set to true for defaults, or provide custom configuration. */ - fetch?: boolean | FetchOptions; + fetchOptions?: boolean | FetchOptions; + customFetch?: typeof fetch; } - -// use `any` when `lib: "dom"` is included in tsconfig.json, -// as dispatcher property does not exist in RequestInit in that case -export type Dispatcher = (typeof globalThis extends { onmessage: any } - ? any - : RequestInit)["dispatcher"]; - export interface FetchOptions { - /** Allow custom dispatcher */ - dispatcher?: Dispatcher; /** Fetch request options */ requestOptions?: RequestInit; /** Called before making the fetch request */ diff --git a/lib/http-proxy/passes/web-incoming.ts b/lib/http-proxy/passes/web-incoming.ts index 15ddc3f..3c8c681 100644 --- a/lib/http-proxy/passes/web-incoming.ts +++ b/lib/http-proxy/passes/web-incoming.ts @@ -86,7 +86,7 @@ export function stream( // And we begin! server.emit("start", req, res, options.target || options.forward!); - if (options.fetch || process.env.FORCE_FETCH_PATH === "true") { + if (options.fetchOptions || process.env.FORCE_FETCH_PATH === "true") { return stream2(req, res, options, _, server, cb); } @@ -226,10 +226,10 @@ async function stream2( handleError(err); }); - const fetchOptions = options.fetch === true ? ({} as FetchOptions) : options.fetch; - if (!fetchOptions) { - throw new Error("stream2 called without fetch options"); - } + const customFetch = options.customFetch || fetch; + + const fetchOptions = (options.fetchOptions === true ? {} : options.fetchOptions) as FetchOptions; + if (options.forward) { const outgoingOptions = common.setupOutgoing(options.ssl || {}, options, req, "forward"); @@ -238,9 +238,6 @@ async function stream2( method: outgoingOptions.method, }; - if (fetchOptions.dispatcher) { - requestOptions.dispatcher = fetchOptions.dispatcher; - } // Handle request body if (options.buffer) { @@ -261,7 +258,7 @@ async function stream2( } try { - const result = await fetch(new URL(outgoingOptions.url).origin + outgoingOptions.path, requestOptions); + const result = await customFetch(new URL(outgoingOptions.url).origin + outgoingOptions.path, requestOptions); // Call onAfterResponse callback for forward requests (though they typically don't expect responses) if (fetchOptions.onAfterResponse) { @@ -294,10 +291,6 @@ async function stream2( ...fetchOptions.requestOptions, }; - if (fetchOptions.dispatcher) { - requestOptions.dispatcher = fetchOptions.dispatcher; - } - if (options.auth) { requestOptions.headers = { ...requestOptions.headers, @@ -323,7 +316,7 @@ async function stream2( } try { - const response = await fetch(new URL(outgoingOptions.url).origin + outgoingOptions.path, requestOptions); + const response = await customFetch(new URL(outgoingOptions.url).origin + outgoingOptions.path, requestOptions); // Call onAfterResponse callback after receiving the response if (fetchOptions.onAfterResponse) { diff --git a/lib/test/http/customFetch.test.ts b/lib/test/http/customFetch.test.ts new file mode 100644 index 0000000..78a7716 --- /dev/null +++ b/lib/test/http/customFetch.test.ts @@ -0,0 +1,71 @@ +/* +pnpm test proxy-http2-to-http2.test.ts + +*/ + +import * as http2 from "node:http2"; +import * as httpProxy from "../.."; +import getPort from "../get-port"; +import { join } from "node:path"; +import { readFile } from "node:fs/promises"; +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { Agent, fetch } from "undici"; + +const TestAgent = new Agent({ allowH2: true, connect: { rejectUnauthorized: false } }); + +const fixturesDir = join(__dirname, "..", "fixtures"); + +const customFetch: typeof fetch = (url, options) => { + return fetch(url, {...options, dispatcher: TestAgent as any }); +}; + +describe("Basic example of proxying over HTTP2 to a target HTTP2 with custom Fetch option server", () => { + let ports: Record<"http2" | "proxy", number>; + beforeAll(async () => { + // Gets ports + ports = { http2: await getPort(), proxy: await getPort() }; + }); + + const servers: any = {}; + let ssl: { key: string; cert: string }; + + it("Create the target HTTP2 server", async () => { + ssl = { + key: await readFile(join(fixturesDir, "agent2-key.pem"), "utf8"), + cert: await readFile(join(fixturesDir, "agent2-cert.pem"), "utf8"), + }; + servers.https = http2 + .createSecureServer(ssl, (_req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.write("hello over http2\n"); + res.end(); + }) + .listen(ports.http2); + }); + + it("Create the HTTPS proxy server", async () => { + servers.proxy = httpProxy + .createServer({ + target: `https://localhost:${ports.http2}`, + ssl, + customFetch: customFetch, + fetchOptions: true, + }) + .listen(ports.proxy); + }); + + it("Use fetch to test direct non-proxied http2 server", async () => { + const r = await (await fetch(`https://localhost:${ports.http2}`, { dispatcher: TestAgent })).text(); + expect(r).toContain("hello over http2"); + }); + + it("Use fetch to test the proxy server", async () => { + const r = await (await fetch(`https://localhost:${ports.proxy}`, { dispatcher: TestAgent })).text(); + expect(r).toContain("hello over http2"); + }); + + afterAll(async () => { + // cleanup + Object.values(servers).map((x: any) => x?.close()); + }); +}); diff --git a/lib/test/http/proxy-callbacks.test.ts b/lib/test/http/proxy-callbacks.test.ts index e4e296f..2eeae43 100644 --- a/lib/test/http/proxy-callbacks.test.ts +++ b/lib/test/http/proxy-callbacks.test.ts @@ -45,10 +45,11 @@ describe("Fetch callback functions (onBeforeRequest and onAfterResponse)", () => const proxy = httpProxy.createServer({ target: `http://localhost:${ports.target}`, - fetch: { - dispatcher: new Agent({ - allowH2: true - }) as any, // Enable undici code path + fetchOptions: { + requestOptions: { + dispatcher: new Agent({ + allowH2: true + }) }, onBeforeRequest: async (requestOptions, _req, _res, _options) => { onBeforeRequestCalled = true; // Modify the outgoing request diff --git a/lib/test/http/proxy-http2-to-http2.test.ts b/lib/test/http/proxy-http2-to-http2.test.ts index 01433b2..4207bab 100644 --- a/lib/test/http/proxy-http2-to-http2.test.ts +++ b/lib/test/http/proxy-http2-to-http2.test.ts @@ -44,7 +44,7 @@ describe("Basic example of proxying over HTTP2 to a target HTTP2 server", () => .createServer({ target: `https://localhost:${ports.http2}`, ssl, - fetch: { dispatcher: TestAgent as any }, + fetchOptions: { requestOptions: { dispatcher: TestAgent as any } }, // without secure false, clients will fail and this is broken: secure: false, }) From e776e7bba73ed9bb1cdd4d82e9b42310f4d3a9f7 Mon Sep 17 00:00:00 2001 From: vhess Date: Wed, 19 Nov 2025 10:42:23 +0100 Subject: [PATCH 2/4] rename customFetch to fetch and address some documentation errors --- README.md | 23 ++++++++++++----------- lib/http-proxy/index.ts | 4 ++-- lib/http-proxy/passes/web-incoming.ts | 2 +- lib/test/http/customFetch.test.ts | 3 +-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f14c208..0e6d24f 100644 --- a/README.md +++ b/README.md @@ -268,7 +268,7 @@ import { Agent } from "undici"; // Create a proxy server with fetch and HTTP/2 support const proxy = createProxyServer({ target: "https://127.0.0.1:5050", - fetch: { + fetchOptions: { requestOptions: {dispatcher: new Agent({ allowH2: true })}, // Modify the request before it's sent onBeforeRequest: async (requestOptions, req, res, options) => { @@ -472,7 +472,7 @@ const proxy = createProxyServer({ // Shorthand to enable fetch with defaults const proxy = createProxyServer({ target: "https://http2-server.example.com", - fetch: true // Uses default fetch configuration + fetch // Uses default fetch configuration }); ``` @@ -482,7 +482,7 @@ const proxy = createProxyServer({ const proxy = createProxyServer({ target: "https://api.example.com", fetchOptions: { - requestOptions: { + requestOptions: { // Use undici's Agent for HTTP/2 support dispatcher: new Agent({ allowH2: true, @@ -524,13 +524,14 @@ const proxy = createProxyServer({ key: readFileSync("server-key.pem"), cert: readFileSync("server-cert.pem") }, - fetch: { - dispatcher: new Agent({ - allowH2: true, - connect: { rejectUnauthorized: false } - }) + fetchOptions: { + requestOptions: { + dispatcher: new Agent({ + allowH2: true, + connect: { rejectUnauthorized: false } + }) + } }, - secure: false // Skip SSL verification for self-signed certs }).listen(8443); ``` @@ -757,8 +758,8 @@ import { Agent } from "undici"; const proxy = createProxyServer({ target: "https://api.example.com", - fetch: { - dispatcher: new Agent({ allowH2: true }), + fetchOptions: { + requestOptions: {dispatcher: new Agent({ allowH2: true })}, // Called before making the fetch request onBeforeRequest: async (requestOptions, req, res, options) => { // Modify the outgoing request diff --git a/lib/http-proxy/index.ts b/lib/http-proxy/index.ts index 9d554e3..29be87b 100644 --- a/lib/http-proxy/index.ts +++ b/lib/http-proxy/index.ts @@ -98,8 +98,8 @@ export interface ServerOptions { */ ca?: string; /** Enable using fetch for proxy requests. Set to true for defaults, or provide custom configuration. */ - fetchOptions?: boolean | FetchOptions; - customFetch?: typeof fetch; + fetchOptions?: FetchOptions; + fetch?: typeof fetch; } export interface FetchOptions { /** Fetch request options */ diff --git a/lib/http-proxy/passes/web-incoming.ts b/lib/http-proxy/passes/web-incoming.ts index 3c8c681..6619901 100644 --- a/lib/http-proxy/passes/web-incoming.ts +++ b/lib/http-proxy/passes/web-incoming.ts @@ -86,7 +86,7 @@ export function stream( // And we begin! server.emit("start", req, res, options.target || options.forward!); - if (options.fetchOptions || process.env.FORCE_FETCH_PATH === "true") { + if (options.fetch ||options.fetchOptions || process.env.FORCE_FETCH_PATH === "true") { return stream2(req, res, options, _, server, cb); } diff --git a/lib/test/http/customFetch.test.ts b/lib/test/http/customFetch.test.ts index 78a7716..0505150 100644 --- a/lib/test/http/customFetch.test.ts +++ b/lib/test/http/customFetch.test.ts @@ -48,8 +48,7 @@ describe("Basic example of proxying over HTTP2 to a target HTTP2 with custom Fet .createServer({ target: `https://localhost:${ports.http2}`, ssl, - customFetch: customFetch, - fetchOptions: true, + fetch: customFetch, }) .listen(ports.proxy); }); From c15085e55eb54d05289650e8bde575bd7b07bebe Mon Sep 17 00:00:00 2001 From: vhess Date: Wed, 19 Nov 2025 10:49:27 +0100 Subject: [PATCH 3/4] update custom fetch with new naming --- lib/http-proxy/passes/web-incoming.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/http-proxy/passes/web-incoming.ts b/lib/http-proxy/passes/web-incoming.ts index 6619901..0f1da56 100644 --- a/lib/http-proxy/passes/web-incoming.ts +++ b/lib/http-proxy/passes/web-incoming.ts @@ -226,9 +226,9 @@ async function stream2( handleError(err); }); - const customFetch = options.customFetch || fetch; + const customFetch = options.fetch || fetch; - const fetchOptions = (options.fetchOptions === true ? {} : options.fetchOptions) as FetchOptions; + const fetchOptions = options.fetchOptions ?? {} as FetchOptions; if (options.forward) { From 28703b8ba41da67ecffd54fc6912fc9d11853a11 Mon Sep 17 00:00:00 2001 From: vhess Date: Wed, 19 Nov 2025 10:50:40 +0100 Subject: [PATCH 4/4] fix: resolve type errors (with shameful any type) --- lib/test/http/customFetch.test.ts | 2 +- lib/test/http/proxy-callbacks.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/test/http/customFetch.test.ts b/lib/test/http/customFetch.test.ts index 0505150..2bbd140 100644 --- a/lib/test/http/customFetch.test.ts +++ b/lib/test/http/customFetch.test.ts @@ -48,7 +48,7 @@ describe("Basic example of proxying over HTTP2 to a target HTTP2 with custom Fet .createServer({ target: `https://localhost:${ports.http2}`, ssl, - fetch: customFetch, + fetch: customFetch as any, }) .listen(ports.proxy); }); diff --git a/lib/test/http/proxy-callbacks.test.ts b/lib/test/http/proxy-callbacks.test.ts index 2eeae43..92cca2e 100644 --- a/lib/test/http/proxy-callbacks.test.ts +++ b/lib/test/http/proxy-callbacks.test.ts @@ -49,7 +49,7 @@ describe("Fetch callback functions (onBeforeRequest and onAfterResponse)", () => requestOptions: { dispatcher: new Agent({ allowH2: true - }) }, + }) as any }, onBeforeRequest: async (requestOptions, _req, _res, _options) => { onBeforeRequestCalled = true; // Modify the outgoing request