Skip to content

Commit 0a3281a

Browse files
authored
Merge pull request #34 from Corvince/http2-incoming
POC: Request through undici
2 parents ce8f922 + a524a26 commit 0a3281a

18 files changed

+1857
-987
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,6 @@ jobs:
4242
- run: npm install -g pnpm
4343
- run: pnpm install
4444
- run: pnpm build
45-
- run: pnpm test
45+
- name: Test native HTTP code path
46+
run: pnpm test-native
47+

README.md

Lines changed: 233 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Contributors:
3030
October 8, 2025 STATUS compared to [http-proxy](https://www.npmjs.com/package/http-proxy) and [httpxy](https://www.npmjs.com/package/httpxy):
3131

3232
- Library entirely rewritten in Typescript in a modern style, with many typings added internally and strict mode enabled.
33+
- **HTTP/2 Support**: Full HTTP/2 support via fetch API with callback-based request/response lifecycle hooks.
3334
- All dependent packages updated to latest versions, addressing all security vulnerabilities according to `pnpm audit`.
3435
- Code rewritten to not use deprecated/insecure API's, e.g., using `URL` instead of `parse`.
3536
- Fixed multiple socket leaks in the Websocket proxy code, going beyond [http-proxy-node16](https://www.npmjs.com/package/http-proxy-node16) to also instrument and logging socket counts. Also fixed an issue with uncatchable errors when using websockets.
@@ -90,7 +91,9 @@ This is the original user's guide, but with various updates.
9091
- [Setup a stand-alone proxy server with latency](#setup-a-stand-alone-proxy-server-with-latency)
9192
- [Using HTTPS](#using-https)
9293
- [Proxying WebSockets](#proxying-websockets)
94+
- [HTTP/2 Support with Fetch](#http2-support-with-fetch)
9395
- [Options](#options)
96+
- [Configuration Compatibility](#configuration-compatibility)
9497
- [Listening for proxy events](#listening-for-proxy-events)
9598
- [Shutdown](#shutdown)
9699
- [Miscellaneous](#miscellaneous)
@@ -116,6 +119,10 @@ import { createProxyServer } from "http-proxy-3";
116119
const proxy = createProxyServer(options); // See below
117120
```
118121

122+
http-proxy-3 supports two request processing paths:
123+
- **Native Path**: Uses Node.js native `http`/`https` modules (default)
124+
- **Fetch Path**: Uses fetch API for HTTP/2 support (when `fetch` option is provided)
125+
119126
Unless listen(..) is invoked on the object, this does not create a webserver. See below.
120127

121128
An object is returned with four methods:
@@ -219,6 +226,8 @@ server.listen(5050);
219226
This example shows how you can proxy a request using your own HTTP server that
220227
modifies the outgoing proxy request by adding a special header.
221228

229+
##### Using Traditional Events (Native HTTP/HTTPS)
230+
222231
```js
223232
import * as http from "node:http";
224233
import { createProxyServer } from "http-proxy-3";
@@ -249,6 +258,39 @@ console.log("listening on port 5050");
249258
server.listen(5050);
250259
```
251260

261+
##### Using Callbacks (Fetch/HTTP/2)
262+
263+
```js
264+
import * as http from "node:http";
265+
import { createProxyServer } from "http-proxy-3";
266+
import { Agent } from "undici";
267+
268+
// Create a proxy server with fetch and HTTP/2 support
269+
const proxy = createProxyServer({
270+
target: "https://127.0.0.1:5050",
271+
fetch: {
272+
dispatcher: new Agent({ allowH2: true }),
273+
// Modify the request before it's sent
274+
onBeforeRequest: async (requestOptions, req, res, options) => {
275+
requestOptions.headers['X-Special-Proxy-Header'] = 'foobar';
276+
requestOptions.headers['X-HTTP2-Enabled'] = 'true';
277+
},
278+
// Access the response after it's received
279+
onAfterResponse: async (response, req, res, options) => {
280+
console.log(`Proxied ${req.url} -> ${response.status}`);
281+
}
282+
}
283+
});
284+
285+
const server = http.createServer((req, res) => {
286+
// The headers are modified via the onBeforeRequest callback
287+
proxy.web(req, res);
288+
});
289+
290+
console.log("listening on port 5050");
291+
server.listen(5050);
292+
```
293+
252294
**[Back to top](#table-of-contents)**
253295

254296
#### Modify a response from a proxied server
@@ -399,6 +441,109 @@ proxyServer.listen(8015);
399441

400442
**[Back to top](#table-of-contents)**
401443

444+
#### HTTP/2 Support with Fetch
445+
446+
> **⚠️ Experimental Feature**: The fetch code path for HTTP/2 support is currently experimental. While it provides HTTP/2 functionality and has comprehensive test coverage, the API and behavior may change in future versions. Use with caution in production environments.
447+
448+
http-proxy-3 supports HTTP/2 through the native fetch API. When fetch is enabled, the proxy can communicate with HTTP/2 servers. The fetch code path is runtime-agnostic and works across different JavaScript runtimes (Node.js, Deno, Bun, etc.). However, this means HTTP/2 support depends on the runtime. Deno enables HTTP/2 by default, Bun currently does not and Node.js requires to set a different dispatcher. See next section for Node.js details.
449+
450+
451+
##### Basic HTTP/2 Setup
452+
453+
```js
454+
import { createProxyServer } from "http-proxy-3";
455+
import { Agent, setGlobalDispatcher } from "undici";
456+
457+
// Either enable HTTP/2 for all fetch operations
458+
setGlobalDispatcher(new Agent({ allowH2: true }));
459+
460+
// Or create a proxy with HTTP/2 support using fetch
461+
const proxy = createProxyServer({
462+
target: "https://http2-server.example.com",
463+
fetch: {
464+
dispatcher: new Agent({ allowH2: true })
465+
}
466+
});
467+
```
468+
469+
##### Simple Fetch Enablement
470+
471+
```js
472+
// Shorthand to enable fetch with defaults
473+
const proxy = createProxyServer({
474+
target: "https://http2-server.example.com",
475+
fetch: true // Uses default fetch configuration
476+
});
477+
```
478+
479+
##### Advanced Configuration with Callbacks
480+
481+
```js
482+
const proxy = createProxyServer({
483+
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+
}),
493+
// Additional fetch request options
494+
requestOptions: {
495+
headersTimeout: 30000,
496+
bodyTimeout: 60000
497+
},
498+
// Called before making the fetch request
499+
onBeforeRequest: async (requestOptions, req, res, options) => {
500+
// Modify outgoing request
501+
requestOptions.headers['X-API-Key'] = 'your-api-key';
502+
requestOptions.headers['X-Request-ID'] = Math.random().toString(36);
503+
},
504+
// Called after receiving the fetch response
505+
onAfterResponse: async (response, req, res, options) => {
506+
// Access full response object
507+
console.log(`Status: ${response.status}`);
508+
console.log('Headers:', response.headers);
509+
// Note: response.body is a stream that will be piped to res automatically
510+
}
511+
}
512+
});
513+
```
514+
515+
##### HTTP/2 with HTTPS Proxy
516+
517+
```js
518+
import { readFileSync } from "node:fs";
519+
import { Agent } from "undici";
520+
521+
const proxy = createProxyServer({
522+
target: "https://http2-target.example.com",
523+
ssl: {
524+
key: readFileSync("server-key.pem"),
525+
cert: readFileSync("server-cert.pem")
526+
},
527+
fetch: {
528+
dispatcher: new Agent({
529+
allowH2: true,
530+
connect: { rejectUnauthorized: false }
531+
})
532+
},
533+
secure: false // Skip SSL verification for self-signed certs
534+
}).listen(8443);
535+
```
536+
537+
538+
**Important Notes:**
539+
- When `fetch` option is provided, the proxy uses the fetch API instead of Node.js native `http`/`https` modules
540+
- To enable HTTP/2, pass a dispatcher (e.g., from undici with `allowH2: true`) in the fetch configuration
541+
- The `onBeforeRequest` and `onAfterResponse` callbacks are only available in the fetch code path
542+
- Traditional `proxyReq` and `proxyRes` events are not emitted in the fetch path - use the callbacks instead
543+
- The fetch approach is runtime-agnostic and doesn't require undici as a dependency for basic HTTP/1.1 proxying
544+
545+
**[Back to top](#table-of-contents)**
546+
402547
### Options
403548

404549
`httpProxy.createProxyServer` supports the following options:
@@ -492,6 +637,14 @@ proxyServer.listen(8015);
492637
};
493638
```
494639
640+
- **ca**: Optionally override the trusted CA certificates. This is passed to https.request.
641+
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
645+
- `onBeforeRequest`: Async callback called before making the fetch request
646+
- `onAfterResponse`: Async callback called after receiving the fetch response
647+
495648
**NOTE:**
496649
`options.ws` and `options.ssl` are optional.
497650
`options.target` and `options.forward` cannot both be missing
@@ -503,6 +656,54 @@ If you are using the `proxyServer.listen` method, the following options are also
503656
504657
**[Back to top](#table-of-contents)**
505658
659+
### Configuration Compatibility
660+
661+
The following table shows which configuration options are compatible with different code paths:
662+
663+
| Option | Native HTTP/HTTPS | Fetch/HTTP/2 | Notes |
664+
|--------|-------------------|---------------|--------|
665+
| `target` | ✅ | ✅ | Core option, works in both paths |
666+
| `forward` | ✅ | ✅ | Core option, works in both paths |
667+
| `agent` | ✅ | ❌ | Native agents only, use `fetch.dispatcher` instead |
668+
| `ssl` | ✅ | ✅ | HTTPS server configuration |
669+
| `ws` | ✅ | ❌ | WebSocket proxying uses native path only |
670+
| `xfwd` | ✅ | ✅ | X-Forwarded headers |
671+
| `secure` | ✅ | ❌¹ | SSL certificate verification |
672+
| `toProxy` | ✅ | ✅ | Proxy-to-proxy configuration |
673+
| `prependPath` | ✅ | ✅ | Path manipulation |
674+
| `ignorePath` | ✅ | ✅ | Path manipulation |
675+
| `localAddress` | ✅ | ✅ | Local interface binding |
676+
| `changeOrigin` | ✅ | ✅ | Host header rewriting |
677+
| `preserveHeaderKeyCase` | ✅ | ✅ | Header case preservation |
678+
| `auth` | ✅ | ✅ | Basic authentication |
679+
| `hostRewrite` | ✅ | ✅ | Redirect hostname rewriting |
680+
| `autoRewrite` | ✅ | ✅ | Automatic redirect rewriting |
681+
| `protocolRewrite` | ✅ | ✅ | Protocol rewriting on redirects |
682+
| `cookieDomainRewrite` | ✅ | ✅ | Cookie domain rewriting |
683+
| `cookiePathRewrite` | ✅ | ✅ | Cookie path rewriting |
684+
| `headers` | ✅ | ✅ | Extra headers to add |
685+
| `proxyTimeout` | ✅ | ✅ | Outgoing request timeout |
686+
| `timeout` | ✅ | ✅ | Incoming request timeout |
687+
| `followRedirects` | ✅ | ✅ | Redirect following |
688+
| `selfHandleResponse` | ✅ | ✅ | Manual response handling |
689+
| `buffer` | ✅ | ✅ | Request body stream |
690+
| `method` | ✅ | ✅ | HTTP method override |
691+
| `ca` | ✅ | ✅ | Custom CA certificates |
692+
| `fetch` | ❌ | ✅ | Fetch-specific configuration |
693+
694+
**Notes:**
695+
- ¹ `secure` is not directly supported in the fetch path. Instead, use `fetch.dispatcher` with `{connect: {rejectUnauthorized: false}}` to disable SSL certificate verification (e.g., for self-signed certificates).
696+
697+
**Code Path Selection:**
698+
- **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)
700+
701+
**Event Compatibility:**
702+
- **Native Path**: Emits traditional events (`proxyReq`, `proxyRes`, `proxyReqWs`)
703+
- **Fetch Path**: Uses callback functions (`onBeforeRequest`, `onAfterResponse`) instead of events
704+
705+
**[Back to top](#table-of-contents)**
706+
506707
### Listening for proxy events
507708
508709
- `error`: The error event is emitted if the request to the target fail. **We do not do any error handling of messages passed between client and proxy, and messages passed between proxy and target, so it is recommended that you listen on errors and handle them.**
@@ -513,11 +714,13 @@ If you are using the `proxyServer.listen` method, the following options are also
513714
- `close`: This event is emitted once the proxy websocket was closed.
514715
- (DEPRECATED) `proxySocket`: Deprecated in favor of `open`.
515716
717+
**Note**: When using the fetch code path (HTTP/2), the `proxyReq` and `proxyRes` events are **not** emitted. Instead, use the `onBeforeRequest` and `onAfterResponse` callback functions in the `fetch` configuration.
718+
719+
#### Traditional Events (Native HTTP/HTTPS path)
720+
516721
```js
517722
import { createProxyServer } from "http-proxy-3";
518-
// Error example
519-
//
520-
// Http Proxy Server with bad target
723+
521724
const proxy = createProxyServer({
522725
target: "http://localhost:9005",
523726
});
@@ -529,7 +732,6 @@ proxy.on("error", (err, req, res) => {
529732
res.writeHead(500, {
530733
"Content-Type": "text/plain",
531734
});
532-
533735
res.end("Something went wrong. And we are reporting a custom error message.");
534736
});
535737
@@ -546,6 +748,33 @@ proxy.on("open", (proxySocket) => {
546748
// listen for messages coming FROM the target here
547749
proxySocket.on("data", hybiParseAndLogMessage);
548750
});
751+
```
752+
753+
#### Callback Functions (Fetch/HTTP2 path)
754+
755+
```js
756+
import { createProxyServer } from "http-proxy-3";
757+
import { Agent } from "undici";
758+
759+
const proxy = createProxyServer({
760+
target: "https://api.example.com",
761+
fetch: {
762+
dispatcher: new Agent({ allowH2: true }),
763+
// Called before making the fetch request
764+
onBeforeRequest: async (requestOptions, req, res, options) => {
765+
// Modify the outgoing request
766+
requestOptions.headers['X-Custom-Header'] = 'added-by-callback';
767+
console.log('Making request to:', requestOptions.headers.host);
768+
},
769+
// Called after receiving the fetch response
770+
onAfterResponse: async (response, req, res, options) => {
771+
// Access the full response object
772+
console.log(`Response: ${response.status}`, response.headers);
773+
// Note: response.body is a stream that will be piped to res automatically
774+
}
775+
}
776+
});
777+
```
549778

550779
// Listen for the `close` event on `proxy`.
551780
proxy.on("close", (res, socket, head) => {

biome.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
3+
"vcs": {
4+
"enabled": false,
5+
"clientKind": "git",
6+
"useIgnoreFile": false
7+
},
8+
"files": {
9+
"ignoreUnknown": false
10+
},
11+
"formatter": {
12+
"enabled": true,
13+
"indentStyle": "space",
14+
"lineWidth": 120
15+
},
16+
"linter": {
17+
"enabled": true,
18+
"rules": {
19+
"recommended": true
20+
}
21+
},
22+
"javascript": {
23+
"formatter": {
24+
"quoteStyle": "double"
25+
}
26+
},
27+
"assist": {
28+
"enabled": true,
29+
"actions": {
30+
"source": {
31+
"organizeImports": "on"
32+
}
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)