Skip to content
Open
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
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,6 @@ jobs:
- run: npm install -g pnpm
- run: pnpm install
- run: pnpm build
- run: pnpm test
- name: Test native HTTP code path
run: pnpm test-native

262 changes: 258 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Contributors:
October 8, 2025 STATUS compared to [http-proxy](https://www.npmjs.com/package/http-proxy) and [httpxy](https://www.npmjs.com/package/httpxy):

- Library entirely rewritten in Typescript in a modern style, with many typings added internally and strict mode enabled.
- **HTTP/2 Support**: Full HTTP/2 support via fetch API with callback-based request/response lifecycle hooks.
- All dependent packages updated to latest versions, addressing all security vulnerabilities according to `pnpm audit`.
- Code rewritten to not use deprecated/insecure API's, e.g., using `URL` instead of `parse`.
- 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.
Expand Down Expand Up @@ -90,7 +91,9 @@ This is the original user's guide, but with various updates.
- [Setup a stand-alone proxy server with latency](#setup-a-stand-alone-proxy-server-with-latency)
- [Using HTTPS](#using-https)
- [Proxying WebSockets](#proxying-websockets)
- [HTTP/2 Support with Fetch](#http2-support-with-fetch)
- [Options](#options)
- [Configuration Compatibility](#configuration-compatibility)
- [Listening for proxy events](#listening-for-proxy-events)
- [Shutdown](#shutdown)
- [Miscellaneous](#miscellaneous)
Expand All @@ -116,6 +119,10 @@ import { createProxyServer } from "http-proxy-3";
const proxy = createProxyServer(options); // See below
```

http-proxy-3 supports two request processing paths:
- **Native Path**: Uses Node.js native `http`/`https` modules (default)
- **Fetch Path**: Uses fetch API for HTTP/2 support (when `fetch` option is provided)

Unless listen(..) is invoked on the object, this does not create a webserver. See below.

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

##### Using Traditional Events (Native HTTP/HTTPS)

```js
import * as http from "node:http";
import { createProxyServer } from "http-proxy-3";
Expand Down Expand Up @@ -249,6 +258,39 @@ console.log("listening on port 5050");
server.listen(5050);
```

##### Using Callbacks (Fetch/HTTP/2)

```js
import * as http from "node:http";
import { createProxyServer } from "http-proxy-3";
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 }),
// Modify the request before it's sent
onBeforeRequest: async (requestOptions, req, res, options) => {
requestOptions.headers['X-Special-Proxy-Header'] = 'foobar';
requestOptions.headers['X-HTTP2-Enabled'] = 'true';
},
// Access the response after it's received
onAfterResponse: async (response, req, res, options) => {
console.log(`Proxied ${req.url} -> ${response.status}`);
}
}
});

const server = http.createServer((req, res) => {
// The headers are modified via the onBeforeRequest callback
proxy.web(req, res);
});

console.log("listening on port 5050");
server.listen(5050);
```

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

#### Modify a response from a proxied server
Expand Down Expand Up @@ -399,6 +441,133 @@ proxyServer.listen(8015);

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

#### HTTP/2 Support with Fetch

> **⚠️ 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.

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.


##### Basic HTTP/2 Setup

```js
import { createProxyServer } from "http-proxy-3";
import { Agent, setGlobalDispatcher } from "undici";

// Either enable HTTP/2 for all fetch operations
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 })
}
});
```

##### Simple Fetch Enablement

```js
// Shorthand to enable fetch with defaults
const proxy = createProxyServer({
target: "https://http2-server.example.com",
fetch: true // Uses default fetch configuration
});
```

##### Advanced Configuration with Callbacks

```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
requestOptions: {
headersTimeout: 30000,
bodyTimeout: 60000
},
// Called before making the fetch request
onBeforeRequest: async (requestOptions, req, res, options) => {
// Modify outgoing request
requestOptions.headers['X-API-Key'] = 'your-api-key';
requestOptions.headers['X-Request-ID'] = Math.random().toString(36);
},
// Called after receiving the fetch response
onAfterResponse: async (response, req, res, options) => {
// Access full response object
console.log(`Status: ${response.status}`);
console.log('Headers:', response.headers);
// Note: response.body is a stream that will be piped to res automatically
}
}
});
```

##### HTTP/2 with HTTPS Proxy

```js
import { readFileSync } from "node:fs";
import { Agent } from "undici";

const proxy = createProxyServer({
target: "https://http2-target.example.com",
ssl: {
key: readFileSync("server-key.pem"),
cert: readFileSync("server-cert.pem")
},
fetch: {
dispatcher: new Agent({
allowH2: true,
connect: { rejectUnauthorized: false }
})
},
secure: false // Skip SSL verification for self-signed certs
}).listen(8443);
```

##### Using Custom Fetch Implementation

```js
import { createProxyServer } from "http-proxy-3";
import { fetch as undiciFetch, Agent } from "undici";

// Wrap undici's fetch with custom configuration
function customFetch(url, opts) {
opts = opts || {};
opts.dispatcher = new Agent({ allowH2: true });
return undiciFetch(url, opts);
}

const proxy = createProxyServer({
target: "https://api.example.com",
fetch: {
// Pass your custom fetch implementation
customFetch,
onBeforeRequest: async (requestOptions, req, res, options) => {
requestOptions.headers['X-Custom'] = 'value';
}
}
});
Comment on lines +553 to +559
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

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

The documentation shows customFetch being used in the example, but this option is not implemented in the actual code (the stream2 function always uses the global fetch). Either implement support for this option or remove it from the documentation to avoid confusion.

Copilot uses AI. Check for mistakes.
```

**Important Notes:**
- When `fetch` option is provided, the proxy uses the fetch API instead of Node.js native `http`/`https` modules
- To enable HTTP/2, pass a dispatcher (e.g., from undici with `allowH2: true`) in the fetch configuration
- The `onBeforeRequest` and `onAfterResponse` callbacks are only available in the fetch code path
- Traditional `proxyReq` and `proxyRes` events are not emitted in the fetch path - use the callbacks instead
- The fetch approach is runtime-agnostic and doesn't require undici as a dependency for basic HTTP/1.1 proxying

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

### Options

`httpProxy.createProxyServer` supports the following options:
Expand Down Expand Up @@ -492,6 +661,15 @@ proxyServer.listen(8015);
};
```

- **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
- `onBeforeRequest`: Async callback called before making the fetch request
- `onAfterResponse`: Async callback called after receiving the fetch response
- `customFetch`: Custom fetch implementation to use instead of the global fetch

**NOTE:**
`options.ws` and `options.ssl` are optional.
`options.target` and `options.forward` cannot both be missing
Expand All @@ -503,6 +681,54 @@ If you are using the `proxyServer.listen` method, the following options are also

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

### Configuration Compatibility

The following table shows which configuration options are compatible with different code paths:

| Option | Native HTTP/HTTPS | Fetch/HTTP/2 | Notes |
|--------|-------------------|---------------|--------|
| `target` | ✅ | ✅ | Core option, works in both paths |
| `forward` | ✅ | ✅ | Core option, works in both paths |
| `agent` | ✅ | ❌ | Native agents only, use `fetch.dispatcher` instead |
| `ssl` | ✅ | ✅ | HTTPS server configuration |
| `ws` | ✅ | ❌ | WebSocket proxying uses native path only |
| `xfwd` | ✅ | ✅ | X-Forwarded headers |
| `secure` | ✅ | ❌¹ | SSL certificate verification |
| `toProxy` | ✅ | ✅ | Proxy-to-proxy configuration |
| `prependPath` | ✅ | ✅ | Path manipulation |
| `ignorePath` | ✅ | ✅ | Path manipulation |
| `localAddress` | ✅ | ✅ | Local interface binding |
| `changeOrigin` | ✅ | ✅ | Host header rewriting |
| `preserveHeaderKeyCase` | ✅ | ✅ | Header case preservation |
| `auth` | ✅ | ✅ | Basic authentication |
| `hostRewrite` | ✅ | ✅ | Redirect hostname rewriting |
| `autoRewrite` | ✅ | ✅ | Automatic redirect rewriting |
| `protocolRewrite` | ✅ | ✅ | Protocol rewriting on redirects |
| `cookieDomainRewrite` | ✅ | ✅ | Cookie domain rewriting |
| `cookiePathRewrite` | ✅ | ✅ | Cookie path rewriting |
| `headers` | ✅ | ✅ | Extra headers to add |
| `proxyTimeout` | ✅ | ✅ | Outgoing request timeout |
| `timeout` | ✅ | ✅ | Incoming request timeout |
| `followRedirects` | ✅ | ✅ | Redirect following |
| `selfHandleResponse` | ✅ | ✅ | Manual response handling |
| `buffer` | ✅ | ✅ | Request body stream |
| `method` | ✅ | ✅ | HTTP method override |
| `ca` | ✅ | ✅ | Custom CA certificates |
| `fetch` | ❌ | ✅ | Fetch-specific configuration |

**Notes:**
- ¹ `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).

**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)

**Event Compatibility:**
- **Native Path**: Emits traditional events (`proxyReq`, `proxyRes`, `proxyReqWs`)
- **Fetch Path**: Uses callback functions (`onBeforeRequest`, `onAfterResponse`) instead of events

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

### Listening for proxy events

- `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.**
Expand All @@ -513,11 +739,13 @@ If you are using the `proxyServer.listen` method, the following options are also
- `close`: This event is emitted once the proxy websocket was closed.
- (DEPRECATED) `proxySocket`: Deprecated in favor of `open`.

**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.

#### Traditional Events (Native HTTP/HTTPS path)

```js
import { createProxyServer } from "http-proxy-3";
// Error example
//
// Http Proxy Server with bad target

const proxy = createProxyServer({
target: "http://localhost:9005",
});
Expand All @@ -529,7 +757,6 @@ proxy.on("error", (err, req, res) => {
res.writeHead(500, {
"Content-Type": "text/plain",
});

res.end("Something went wrong. And we are reporting a custom error message.");
});

Expand All @@ -546,6 +773,33 @@ proxy.on("open", (proxySocket) => {
// listen for messages coming FROM the target here
proxySocket.on("data", hybiParseAndLogMessage);
});
```

#### Callback Functions (Fetch/HTTP2 path)

```js
import { createProxyServer } from "http-proxy-3";
import { Agent } from "undici";

const proxy = createProxyServer({
target: "https://api.example.com",
fetch: {
dispatcher: new Agent({ allowH2: true }),
// Called before making the fetch request
onBeforeRequest: async (requestOptions, req, res, options) => {
// Modify the outgoing request
requestOptions.headers['X-Custom-Header'] = 'added-by-callback';
console.log('Making request to:', requestOptions.headers.host);
},
// Called after receiving the fetch response
onAfterResponse: async (response, req, res, options) => {
// Access the full response object
console.log(`Response: ${response.status}`, response.headers);
// Note: response.body is a stream that will be piped to res automatically
}
}
});
```

// Listen for the `close` event on `proxy`.
proxy.on("close", (res, socket, head) => {
Expand Down
35 changes: 35 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"lineWidth": 120
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
Loading
Loading