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
54 changes: 27 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 })}
}
});
```
Expand All @@ -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
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example is incomplete or unclear. Based on the PR changes, fetch should be set to the global fetch function to use default fetch configuration. This should be fetch: fetch or just importing and using the global fetch. The current syntax fetch without assignment is ambiguous and may not work as intended.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems fine to me as is.

});
```

Expand All @@ -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
},
Expand Down Expand Up @@ -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);
```

Expand Down Expand Up @@ -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:
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation is incorrect. fetchOptions cannot be set to true according to the new TypeScript interface definition. It should be an object of type FetchOptions or undefined. Remove the "Set to true for defaults, or" part, or update the interface to support true as a value.

Suggested change
- **fetchOptions**: Enable fetch API for HTTP/2 support. Set to `true` for defaults, or provide custom configuration:
- **fetchOptions**: Enable fetch API for HTTP/2 support. Provide an object of type `FetchOptions` for custom configuration:

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has a very good point.

- `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

Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -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
Expand Down
12 changes: 2 additions & 10 deletions lib/http-proxy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,18 +98,10 @@ export interface ServerOptions {
*/
ca?: string;
/** Enable using fetch for proxy requests. Set to true for defaults, or provide custom configuration. */
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc comment is outdated. It mentions "Set to true for defaults, or provide custom configuration", but the fetchOptions property only accepts FetchOptions type, not boolean. The comment should be updated to reflect the new API design where fetchOptions is an optional object and fetch is a separate property for custom fetch implementations.

Suggested change
/** Enable using fetch for proxy requests. Set to true for defaults, or provide custom configuration. */
/** Optional configuration object for fetch-based proxy requests. Use this to customize fetch request and response handling. For custom fetch implementations, use the `fetch` property. */

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point

fetch?: boolean | FetchOptions;
fetchOptions?: FetchOptions;
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing JSDoc comment for the new fetch property. This should document that it allows providing a custom fetch implementation (e.g., a wrapper around fetch or from a different package).

Suggested change
fetchOptions?: FetchOptions;
fetchOptions?: FetchOptions;
/**
* Allows providing a custom fetch implementation (e.g., a wrapper around fetch or from a different package).
* If set, this function will be used for proxy requests instead of the default fetch.
*/

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems fine as you have it, since the comment applies to both options (fetch and 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 */
Expand Down
21 changes: 7 additions & 14 deletions lib/http-proxy/passes/web-incoming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after || operator. Should be options.fetch || options.fetchOptions.

Suggested change
if (options.fetch ||options.fetchOptions || process.env.FORCE_FETCH_PATH === "true") {
if (options.fetch || options.fetchOptions || process.env.FORCE_FETCH_PATH === "true") {

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'll run prettier so don't care.

return stream2(req, res, options, _, server, cb);
}

Expand Down Expand Up @@ -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");
Expand All @@ -238,9 +238,6 @@ async function stream2(
method: outgoingOptions.method,
};

if (fetchOptions.dispatcher) {
requestOptions.dispatcher = fetchOptions.dispatcher;
}

// Handle request body
if (options.buffer) {
Expand All @@ -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) {
Expand Down Expand Up @@ -294,10 +291,6 @@ async function stream2(
...fetchOptions.requestOptions,
};

if (fetchOptions.dispatcher) {
requestOptions.dispatcher = fetchOptions.dispatcher;
}

if (options.auth) {
requestOptions.headers = {
...requestOptions.headers,
Expand All @@ -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) {
Expand Down
70 changes: 70 additions & 0 deletions lib/test/http/customFetch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
pnpm test proxy-http2-to-http2.test.ts
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment references the wrong test file. It should reference customFetch.test.ts instead of proxy-http2-to-http2.test.ts.

Suggested change
pnpm test proxy-http2-to-http2.test.ts
pnpm test customFetch.test.ts

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point

*/

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", () => {
Copy link

Copilot AI Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test description has awkward wording with "custom Fetch option server" at the end. It should be "with custom Fetch option" or "server with custom Fetch option".

Suggested change
describe("Basic example of proxying over HTTP2 to a target HTTP2 with custom Fetch option server", () => {
describe("Basic example of proxying over HTTP2 to a target HTTP2 server with custom Fetch option", () => {

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fine either way

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());
});
});
9 changes: 5 additions & 4 deletions lib/test/http/proxy-callbacks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/test/http/proxy-http2-to-http2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down
Loading