diff --git a/README.md b/README.md index 4276a2d..0e6d24f 100644 --- a/README.md +++ b/README.md @@ -268,8 +268,8 @@ 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: { - dispatcher: new Agent({ allowH2: true }), + fetchOptions: { + 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 })} } }); ``` @@ -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 }); ``` @@ -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 - } - }), - // Additional fetch request options + 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 headersTimeout: 30000, bodyTimeout: 60000 }, @@ -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); ``` @@ -639,9 +640,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 +696,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`) @@ -758,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 7a6d08c..29be87b 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?: FetchOptions; + fetch?: 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..0f1da56 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.fetch ||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.fetch || fetch; + + const fetchOptions = 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..2bbd140 --- /dev/null +++ b/lib/test/http/customFetch.test.ts @@ -0,0 +1,70 @@ +/* +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, + fetch: customFetch as any, + }) + .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..92cca2e 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 + }) as any }, 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, })