Skip to content

Commit a13d600

Browse files
author
vhess
committed
Add custom Fetch option and supersede dispatcher option
1 parent 2e0923f commit a13d600

File tree

6 files changed

+102
-46
lines changed

6 files changed

+102
-46
lines changed

README.md

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ import { Agent } from "undici";
269269
const proxy = createProxyServer({
270270
target: "https://127.0.0.1:5050",
271271
fetch: {
272-
dispatcher: new Agent({ allowH2: true }),
272+
requestOptions: {dispatcher: new Agent({ allowH2: true })},
273273
// Modify the request before it's sent
274274
onBeforeRequest: async (requestOptions, req, res, options) => {
275275
requestOptions.headers['X-Special-Proxy-Header'] = 'foobar';
@@ -460,8 +460,8 @@ setGlobalDispatcher(new Agent({ allowH2: true }));
460460
// Or create a proxy with HTTP/2 support using fetch
461461
const proxy = createProxyServer({
462462
target: "https://http2-server.example.com",
463-
fetch: {
464-
dispatcher: new Agent({ allowH2: true })
463+
fetchOptions: {
464+
requestOptions: {dispatcher: new Agent({ allowH2: true })}
465465
}
466466
});
467467
```
@@ -481,17 +481,17 @@ const proxy = createProxyServer({
481481
```js
482482
const proxy = createProxyServer({
483483
target: "https://api.example.com",
484-
fetch: {
485-
// Use undici's Agent for HTTP/2 support
486-
dispatcher: new Agent({
487-
allowH2: true,
488-
connect: {
489-
rejectUnauthorized: false, // For self-signed certs
490-
timeout: 10000
491-
}
492-
}),
484+
fetchOptions: {
485+
requestOptions: {
486+
// Use undici's Agent for HTTP/2 support
487+
dispatcher: new Agent({
488+
allowH2: true,
489+
connect: {
490+
rejectUnauthorized: false, // For self-signed certs
491+
timeout: 10000
492+
}
493+
}),
493494
// Additional fetch request options
494-
requestOptions: {
495495
headersTimeout: 30000,
496496
bodyTimeout: 60000
497497
},
@@ -639,9 +639,8 @@ const proxy = createProxyServer({
639639
640640
- **ca**: Optionally override the trusted CA certificates. This is passed to https.request.
641641
642-
- **fetch**: Enable fetch API for HTTP/2 support. Set to `true` for defaults, or provide custom configuration:
643-
- `dispatcher`: Custom fetch dispatcher (e.g., undici Agent with `allowH2: true` for HTTP/2)
644-
- `requestOptions`: Additional fetch request options
642+
- **fetchOptions**: Enable fetch API for HTTP/2 support. Set to `true` for defaults, or provide custom configuration:
643+
- `requestOptions`: Additional fetch request options (e.g., undici Agent with `allowH2: true` for HTTP/2 as dispatcher)
645644
- `onBeforeRequest`: Async callback called before making the fetch request
646645
- `onAfterResponse`: Async callback called after receiving the fetch response
647646
@@ -696,7 +695,7 @@ The following table shows which configuration options are compatible with differ
696695
697696
**Code Path Selection:**
698697
- **Native Path**: Used by default, supports HTTP/1.1 and WebSockets
699-
- **Fetch Path**: Activated when `fetch` option is provided, supports HTTP/2 (with appropriate dispatcher)
698+
- **Fetch Path**: Activated when `fetchOptions` option is provided, supports HTTP/2 (with appropriate dispatcher)
700699
701700
**Event Compatibility:**
702701
- **Native Path**: Emits traditional events (`proxyReq`, `proxyRes`, `proxyReqWs`)

lib/http-proxy/index.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,10 @@ export interface ServerOptions {
9898
*/
9999
ca?: string;
100100
/** Enable using fetch for proxy requests. Set to true for defaults, or provide custom configuration. */
101-
fetch?: boolean | FetchOptions;
101+
fetchOptions?: boolean | FetchOptions;
102+
customFetch?: typeof fetch;
102103
}
103-
104-
// use `any` when `lib: "dom"` is included in tsconfig.json,
105-
// as dispatcher property does not exist in RequestInit in that case
106-
export type Dispatcher = (typeof globalThis extends { onmessage: any }
107-
? any
108-
: RequestInit)["dispatcher"];
109-
110104
export interface FetchOptions {
111-
/** Allow custom dispatcher */
112-
dispatcher?: Dispatcher;
113105
/** Fetch request options */
114106
requestOptions?: RequestInit;
115107
/** Called before making the fetch request */

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

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export function stream(
8686
// And we begin!
8787
server.emit("start", req, res, options.target || options.forward!);
8888

89-
if (options.fetch || process.env.FORCE_FETCH_PATH === "true") {
89+
if (options.fetchOptions || process.env.FORCE_FETCH_PATH === "true") {
9090
return stream2(req, res, options, _, server, cb);
9191
}
9292

@@ -226,10 +226,10 @@ async function stream2(
226226
handleError(err);
227227
});
228228

229-
const fetchOptions = options.fetch === true ? ({} as FetchOptions) : options.fetch;
230-
if (!fetchOptions) {
231-
throw new Error("stream2 called without fetch options");
232-
}
229+
const customFetch = options.customFetch || fetch;
230+
231+
const fetchOptions = (options.fetchOptions === true ? {} : options.fetchOptions) as FetchOptions;
232+
233233

234234
if (options.forward) {
235235
const outgoingOptions = common.setupOutgoing(options.ssl || {}, options, req, "forward");
@@ -238,9 +238,6 @@ async function stream2(
238238
method: outgoingOptions.method,
239239
};
240240

241-
if (fetchOptions.dispatcher) {
242-
requestOptions.dispatcher = fetchOptions.dispatcher;
243-
}
244241

245242
// Handle request body
246243
if (options.buffer) {
@@ -261,7 +258,7 @@ async function stream2(
261258
}
262259

263260
try {
264-
const result = await fetch(new URL(outgoingOptions.url).origin + outgoingOptions.path, requestOptions);
261+
const result = await customFetch(new URL(outgoingOptions.url).origin + outgoingOptions.path, requestOptions);
265262

266263
// Call onAfterResponse callback for forward requests (though they typically don't expect responses)
267264
if (fetchOptions.onAfterResponse) {
@@ -294,10 +291,6 @@ async function stream2(
294291
...fetchOptions.requestOptions,
295292
};
296293

297-
if (fetchOptions.dispatcher) {
298-
requestOptions.dispatcher = fetchOptions.dispatcher;
299-
}
300-
301294
if (options.auth) {
302295
requestOptions.headers = {
303296
...requestOptions.headers,
@@ -323,7 +316,7 @@ async function stream2(
323316
}
324317

325318
try {
326-
const response = await fetch(new URL(outgoingOptions.url).origin + outgoingOptions.path, requestOptions);
319+
const response = await customFetch(new URL(outgoingOptions.url).origin + outgoingOptions.path, requestOptions);
327320

328321
// Call onAfterResponse callback after receiving the response
329322
if (fetchOptions.onAfterResponse) {

lib/test/http/customFetch.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
pnpm test proxy-http2-to-http2.test.ts
3+
4+
*/
5+
6+
import * as http2 from "node:http2";
7+
import * as httpProxy from "../..";
8+
import getPort from "../get-port";
9+
import { join } from "node:path";
10+
import { readFile } from "node:fs/promises";
11+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
12+
import { Agent, fetch } from "undici";
13+
14+
const TestAgent = new Agent({ allowH2: true, connect: { rejectUnauthorized: false } });
15+
16+
const fixturesDir = join(__dirname, "..", "fixtures");
17+
18+
const customFetch: typeof fetch = (url, options) => {
19+
return fetch(url, {...options, dispatcher: TestAgent as any });
20+
};
21+
22+
describe("Basic example of proxying over HTTP2 to a target HTTP2 with custom Fetch option server", () => {
23+
let ports: Record<"http2" | "proxy", number>;
24+
beforeAll(async () => {
25+
// Gets ports
26+
ports = { http2: await getPort(), proxy: await getPort() };
27+
});
28+
29+
const servers: any = {};
30+
let ssl: { key: string; cert: string };
31+
32+
it("Create the target HTTP2 server", async () => {
33+
ssl = {
34+
key: await readFile(join(fixturesDir, "agent2-key.pem"), "utf8"),
35+
cert: await readFile(join(fixturesDir, "agent2-cert.pem"), "utf8"),
36+
};
37+
servers.https = http2
38+
.createSecureServer(ssl, (_req, res) => {
39+
res.writeHead(200, { "Content-Type": "text/plain" });
40+
res.write("hello over http2\n");
41+
res.end();
42+
})
43+
.listen(ports.http2);
44+
});
45+
46+
it("Create the HTTPS proxy server", async () => {
47+
servers.proxy = httpProxy
48+
.createServer({
49+
target: `https://localhost:${ports.http2}`,
50+
ssl,
51+
customFetch: customFetch,
52+
fetchOptions: true,
53+
})
54+
.listen(ports.proxy);
55+
});
56+
57+
it("Use fetch to test direct non-proxied http2 server", async () => {
58+
const r = await (await fetch(`https://localhost:${ports.http2}`, { dispatcher: TestAgent })).text();
59+
expect(r).toContain("hello over http2");
60+
});
61+
62+
it("Use fetch to test the proxy server", async () => {
63+
const r = await (await fetch(`https://localhost:${ports.proxy}`, { dispatcher: TestAgent })).text();
64+
expect(r).toContain("hello over http2");
65+
});
66+
67+
afterAll(async () => {
68+
// cleanup
69+
Object.values(servers).map((x: any) => x?.close());
70+
});
71+
});

lib/test/http/proxy-callbacks.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,11 @@ describe("Fetch callback functions (onBeforeRequest and onAfterResponse)", () =>
4545

4646
const proxy = httpProxy.createServer({
4747
target: `http://localhost:${ports.target}`,
48-
fetch: {
49-
dispatcher: new Agent({
50-
allowH2: true
51-
}) as any, // Enable undici code path
48+
fetchOptions: {
49+
requestOptions: {
50+
dispatcher: new Agent({
51+
allowH2: true
52+
}) },
5253
onBeforeRequest: async (requestOptions, _req, _res, _options) => {
5354
onBeforeRequestCalled = true;
5455
// Modify the outgoing request

lib/test/http/proxy-http2-to-http2.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe("Basic example of proxying over HTTP2 to a target HTTP2 server", () =>
4444
.createServer({
4545
target: `https://localhost:${ports.http2}`,
4646
ssl,
47-
fetch: { dispatcher: TestAgent as any },
47+
fetchOptions: { requestOptions: { dispatcher: TestAgent as any } },
4848
// without secure false, clients will fail and this is broken:
4949
secure: false,
5050
})

0 commit comments

Comments
 (0)