Full Code of unjs/httpxy for AI

main 9dc2425ecb48 cached
56 files
324.4 KB
84.5k tokens
142 symbols
1 requests
Download .txt
Showing preview only (342K chars total). Download the full file or copy to clipboard to get everything.
Repository: unjs/httpxy
Branch: main
Commit: 9dc2425ecb48
Files: 56
Total size: 324.4 KB

Directory structure:
gitextract_lci7uuca/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── autofix.yml
│       └── checks.yml
├── .gitignore
├── .oxfmtrc.json
├── .oxlintrc.json
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── LICENSE
├── README.md
├── bench/
│   ├── Dockerfile
│   ├── bench.ts
│   ├── package.json
│   ├── src/
│   │   ├── fast-proxy.ts
│   │   ├── fastify.ts
│   │   ├── http-proxy-3.ts
│   │   ├── http-proxy.ts
│   │   ├── httpxy-fetch.ts
│   │   ├── httpxy-server.ts
│   │   └── target.ts
│   └── test.ts
├── build.config.mjs
├── package.json
├── playground/
│   └── index.ts
├── pnpm-workspace.yaml
├── renovate.json
├── src/
│   ├── _utils.ts
│   ├── fetch.ts
│   ├── index.ts
│   ├── middleware/
│   │   ├── _utils.ts
│   │   ├── web-incoming.ts
│   │   ├── web-outgoing.ts
│   │   └── ws-incoming.ts
│   ├── server.ts
│   ├── types.ts
│   └── ws.ts
├── test/
│   ├── _stubs.ts
│   ├── _utils.test.ts
│   ├── _utils.ts
│   ├── fetch.test.ts
│   ├── fixtures/
│   │   ├── agent2-cert.pem
│   │   └── agent2-key.pem
│   ├── http-proxy.test.ts
│   ├── http2-proxy.test.ts
│   ├── https-proxy.test.ts
│   ├── index.test.ts
│   ├── middleware/
│   │   ├── web-incoming.test.ts
│   │   ├── web-outgoing.test.ts
│   │   └── ws-incoming.test.ts
│   ├── server.test.ts
│   ├── types.test-d.ts
│   ├── ws-destroyed-socket.test.ts
│   └── ws.test.ts
├── tsconfig.json
└── vitest.config.mjs

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8

[*.js]
indent_style = space
indent_size = 2

[{package.json,*.yml,*.cjson}]
indent_style = space
indent_size = 2


================================================
FILE: .github/workflows/autofix.yml
================================================
name: autofix.ci
on: { push: {}, pull_request: {} }
permissions: { contents: read }
jobs:
  autofix:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - run: npm i -fg corepack && corepack enable
      - uses: actions/setup-node@v6
        with: { node-version: lts/*, cache: "pnpm" }
      - run: pnpm install
      - run: pnpm fmt
      - uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8
        with: { commit-message: "chore: apply automated updates" }


================================================
FILE: .github/workflows/checks.yml
================================================
name: checks
on: { push: {}, pull_request: {} }
jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - run: npm i -fg corepack && corepack enable
      - uses: actions/setup-node@v6
        with: { node-version: lts/*, cache: "pnpm" }
      - run: pnpm install
      - run: pnpm typecheck
      - run: pnpm vitest --coverage
      - run: pnpm run lint
      - uses: codecov/codecov-action@v6
        with: { token: "${{ secrets.CODECOV_TOKEN }}" }


================================================
FILE: .gitignore
================================================
node_modules
coverage
dist
types
.vscode
.DS_Store
.eslintcache
*.log*
*.env*


================================================
FILE: .oxfmtrc.json
================================================
{
  "$schema": "https://unpkg.com/oxfmt/configuration_schema.json"
}


================================================
FILE: .oxlintrc.json
================================================
{
  "$schema": "https://unpkg.com/oxlint/configuration_schema.json",
  "plugins": ["unicorn", "typescript", "oxc"],
  "rules": {
    "no-unused-vars": "off"
  }
}


================================================
FILE: AGENTS.md
================================================
# httpxy — Agent Guide

Full-featured HTTP/WebSocket proxy for Node.js. Zero production dependencies. Originally forked from [http-party/node-http-proxy](https://github.com/http-party/node-http-proxy).

## API

- **`createProxyServer(opts)`** / **`ProxyServer`** — Traditional event-driven proxy server with middleware pipeline
- **`proxyFetch(addr, input, init?)`** — Web-standard `Request`/`Response` interface for proxying individual requests
- **`proxyUpgrade(addr, req, socket, head?, opts?)`** — Standalone WebSocket upgrade proxy without a `ProxyServer` instance

## Source Architecture (`src/`)

```
src/
├── index.ts              — Re-exports (entry point)
├── types.ts              — ProxyTarget, ProxyServerOptions, ProxyTargetDetailed
├── server.ts             — ProxyServer class (EventEmitter), createProxyServer()
├── fetch.ts              — proxyFetch() using Node.js http module → Web Response
├── upgrade.ts            — proxyUpgrade() standalone WebSocket upgrade proxy
├── _utils.ts             — setupOutgoing(), setupSocket(), joinURL(), cookie/header helpers
└── middleware/
    ├── _utils.ts          — Middleware type definitions (ProxyMiddleware, ProxyOutgoingMiddleware)
    ├── web-incoming.ts    — HTTP request passes: deleteLength → timeout → XHeaders → stream
    ├── web-outgoing.ts    — HTTP response passes: removeChunked → setConnection → setRedirectHostRewrite → writeHeaders → writeStatusCode
    └── ws-incoming.ts     — WebSocket passes: checkMethodAndHeader → XHeaders → stream
```

### Request flow (HTTP)

```
Client → ProxyServer.web() → web-incoming passes → http.request(target) → target server
Target response → web-outgoing passes → client response
```

### Request flow (WebSocket)

```
Client upgrade → ProxyServer.ws() → ws-incoming passes → http.request(target)
Target upgrade → bidirectional socket pipe
```

### Request flow (proxyFetch)

```
proxyFetch(addr, request) → http.request to addr → Web Response
```

### Request flow (proxyUpgrade)

```
proxyUpgrade(addr, req, socket, head) → http.request to addr → upgrade → bidirectional socket pipe
Returns Promise<Socket> (the upstream proxy socket)
```

### Key design patterns

- **Middleware pipeline**: Passes are functions that run in order; returning `true` halts the chain
- **Event-driven**: `ProxyServer` emits lifecycle events (`start`, `proxyReq`, `proxyRes`, `end`, `error`, `open`, `close`)
- **Extensible middleware**: `server.before(type, passName, fn)` / `server.after(type, passName, fn)` to insert custom passes
- **Flexible targets**: TCP (`host:port`), Unix socket (`socketPath`), or URL string

## Behavioral Notes (Source + Tests)

- `proxy.web()` / `proxy.ws()` return a Promise and can either reject (no `error` listener, request/response error) or resolve after `res.close`.
- Per-call options are merged as `{ ...opts, ...server.options }`, so constructor options override per-call options on key conflicts.
- String `target`/`forward` values are normalized to `URL` objects before middleware execution.
- Missing both `target` and `forward` emits `error` with message `"Must provide a proper URL as target"`.
- Middleware names are often empty strings (passes are wrapped arrow functions). Tests use `before("web", "", ...)` / `after("web", "", ...)`.
- `ProxyServer.close()` is a no-op before `listen()`, and sets internal `_server` to `undefined` after close callback.

### HTTP middleware semantics

- Incoming pass order is fixed: `deleteLength -> timeout -> XHeaders -> stream`.
- `deleteLength` applies to both `DELETE` and `OPTIONS` without content length; it sets `content-length: 0` and removes `transfer-encoding`.
- `proxyReq` event is intentionally skipped when request has `expect` header (`100-continue` advisory coverage).
- `selfHandleResponse: true` skips outgoing passes and auto-pipe; callers must finish the response in `proxyRes`.
- `proxyTimeout` aborts upstream request and surfaces timeout errors (tested as `ECONNRESET`).
- `followRedirects: true | number` enables native redirect following (301/302/303/307/308). `true` = max 5 hops, number = custom max.
- On 301/302/303 redirects, method changes to GET and request body is dropped.
- On 307/308 redirects, original method and body are preserved (body is buffered on first request for replay).
- `proxyRes` event fires only for the final (non-redirect) response; `proxyReq` fires for each request including redirects.
- Sensitive headers (`authorization`, `cookie`) are stripped on cross-origin redirects.
- When `followRedirects` is enabled, the request body is tee'd (written to proxy request and buffered simultaneously) rather than piped.

### WebSocket middleware semantics

- Incoming pass order is fixed: `checkMethodAndHeader -> XHeaders -> stream`.
- WS requests must be `GET` with `upgrade: websocket`; otherwise socket is destroyed and the chain stops.
- `proxyReqWs`, `open`, `close`, and deprecated `proxySocket` events are part of tested flow.
- Upgrade response headers preserve repeated headers like multiple `Set-Cookie` values.

### Outgoing response semantics

- Outgoing pass order is fixed: `removeChunked -> setConnection -> setRedirectHostRewrite -> writeHeaders -> writeStatusCode`.
- Redirect rewrite applies on `201`, `301`, `302`, `303`, `307`, `308` only, and only when `Location` host matches `target.host`.
- `hostRewrite` takes precedence over `autoRewrite`; `protocolRewrite` composes with either.
- Cookie rewriting supports string or mapping config (including wildcard `"*"` and empty string for removal).
- `preserveHeaderKeyCase` uses `rawHeaders` when available.

### URL/path handling invariants

- Path joining does not normalize repeated slashes (`/a/b//c` stays as-is).
- `joinURL()` always returns a usable path and avoids duplicate slash insertion.
- `toProxy: true` preserves absolute URL in outgoing path as `"/" + req.url`.
- `ignorePath` drops request path; with `prependPath: false` the outgoing path becomes `/`.

### `proxyFetch` semantics

- `addr` accepts `http://host:port`, `https://host:port`, `unix:/path.sock`, or object form `{ host, port }` / `{ socketPath }`.
- Both HTTP and HTTPS upstream targets are supported. HTTPS is auto-detected from the `addr` string protocol.
- When `addr` is a URL string with a path (e.g. `http://host:port/api`), the path is prepended to the request path via `joinURL()`.
- Redirect mode defaults to `manual`.
- Streaming request bodies are supported (`ReadableStream`) and set `duplex: "half"`.
- Hop-by-hop response headers `transfer-encoding`, `keep-alive`, `connection` are stripped.
- Response body is `null` for `204` and `304`.
- Network and request-body-stream errors reject the Promise.
- Accepts optional `ProxyFetchOptions` as 4th argument with `timeout`, `xfwd`, `changeOrigin`, `agent`, `followRedirects`, and `ssl`.
- `timeout` sets a deadline on the upstream request; rejects with `"Proxy request timed out"` on expiry.
- `xfwd` adds `x-forwarded-for`, `x-forwarded-port`, `x-forwarded-proto`, `x-forwarded-host` derived from the input URL (not from a socket, since there is no incoming connection). Existing headers are not overwritten.
- `changeOrigin` rewrites the `Host` header to match the resolved target address (host:port for TCP, `localhost` for Unix sockets). Accounts for default ports (80 for HTTP, 443 for HTTPS).
- `agent` enables connection pooling/reuse via a custom `http.Agent`. Defaults to `false` (no agent).
- `followRedirects` enables automatic redirect following. `true` = max 5 hops; number = custom max. On 301/302/303 method changes to GET and body is dropped. On 307/308 method and body are preserved (body is buffered). Sensitive headers (`authorization`, `cookie`) are stripped on cross-origin redirects.
- `ssl` passes TLS options to `https.request` (e.g. `{ rejectUnauthorized: false }`).
- `AbortSignal` support is wired through `init.signal` (standard `RequestInit`), aborting the underlying `http.request`.
- Multi-value request headers are preserved as arrays (not flattened by the `Headers` API).
- Body types `ArrayBuffer`, `TypedArray`, and `Blob` are properly converted to `Buffer` before sending.

### `proxyUpgrade` semantics

- Standalone WebSocket upgrade proxy — no `ProxyServer` instance or `EventEmitter` needed.
- `addr` accepts same formats as `proxyFetch`: `http://host:port`, `ws://host:port`, `unix:/path`, or object `{ host, port }` / `{ socketPath }`.
- Validates that the request is a valid WS upgrade (`GET` + `upgrade: websocket`); rejects with error and destroys socket otherwise.
- `xfwd` is enabled by default (unlike `ProxyServer` where it defaults to `false`). Pass `xfwd: false` to disable.
- Supports `xfwd`, `changeOrigin`, `headers`, `ssl`, `secure`, `agent`, `auth`, `prependPath`, `ignorePath`, `toProxy` options via `ProxyUpgradeOptions`.
- Returns `Promise<Socket>` — resolves with the upstream proxy socket on successful upgrade, rejects on connection or socket error.
- If the upstream responds without upgrading (e.g., 404), the response is relayed to the client socket.
- Uses `setupOutgoing()` and `setupSocket()` from shared utils, consistent with `ProxyServer.ws()`.

## Tests (`test/`)

```
test/
├── index.test.ts                  — Main proxy: paths, headers, changeOrigin, xfwd, WebSocket, errors
├── fetch.test.ts                  — proxyFetch: TCP/Unix, GET/POST, redirects, cookies, 204/304, signal, timeout, xfwd, changeOrigin
├── upgrade.test.ts                — proxyUpgrade: WS proxy, addr formats, xfwd, error handling
├── http-proxy.test.ts             — Forward, target, WebSocket, socket.io, SSE, timeouts, error events
├── https-proxy.test.ts            — HTTPS targets, SSL certs, certificate validation
├── _utils.test.ts                 — setupOutgoing, setupSocket, path joining, auth, changeOrigin
├── types.test-d.ts                — TypeScript type assertions (vitest typecheck)
└── middleware/
    ├── web-incoming.test.ts       — deleteLength, timeout, XHeaders
    ├── web-outgoing.test.ts       — Redirect rewrite, writeHeaders, cookies, status codes
    └── ws-incoming.test.ts        — Method/header validation, error handling
```

### Running tests

```bash
pnpm vitest run test/<file>       # Single test file
pnpm vitest run                   # All tests
pnpm test                         # Lint + typecheck + tests with coverage
```

### Test expectations and parity

- The suite includes legacy parity tests ported from `http-party/node-http-proxy` plus project-specific tests (`test/server.test.ts`, `test/fetch.test.ts`, `test/types.test-d.ts`).
- `followRedirects` is natively implemented (no external dependency). See behavioral notes below.
- HTTPS tests rely on local fixtures in `test/fixtures/agent2-*.pem`.

## Tooling

| Tool      | Command          | Notes                                         |
| --------- | ---------------- | --------------------------------------------- |
| Build     | `pnpm build`     | Uses `unbuild` → CJS + ESM + types in `dist/` |
| Dev       | `pnpm dev`       | Vitest watch mode                             |
| Lint      | `pnpm lint`      | `oxlint` + `oxfmt --check`                    |
| Format    | `pnpm fmt`       | `oxlint --fix` + `oxfmt`                      |
| Typecheck | `pnpm typecheck` | `tsgo --noEmit` (native TS preview)           |
| Test      | `pnpm test`      | Full: lint + typecheck + vitest with coverage |

## Key Types

```ts
type ProxyTarget = string | URL | ProxyTargetDetailed;

interface ProxyTargetDetailed {
  host?: string;
  port?: number | string;
  protocol?: string;
  hostname?: string;
  socketPath?: string;
  // TLS: key, passphrase, pfx, cert, ca, ciphers, secureProtocol
}

interface ProxyServerOptions {
  target?: ProxyTarget; // Proxy destination
  forward?: ProxyTarget; // Forward destination
  ws?: boolean; // Enable WebSocket proxying
  xfwd?: boolean; // Add x-forwarded-* headers
  changeOrigin?: boolean; // Rewrite Host header to target
  // ... 30+ more options for paths, headers, cookies, TLS, timeouts
}
```

## Conventions

- ESM only (`"type": "module"`)
- Strict TypeScript with `nodenext` module resolution
- Internal files prefixed with `_` (e.g., `_utils.ts`)
- Tests use `vitest` + `expect.js` assertions
- No production dependencies — Node.js built-ins only
- Semantic commits: `feat(scope):`, `fix(scope):`, `test:`, `chore:`

## Maintenance

- Keep `AGENTS.md` and `README.md` in sync with the current codebase.
- When you discover new behavior, constraints, architecture details, or workflows, document them in `AGENTS.md` for future agents.
- When implementation or design changes affect user-facing usage or project expectations, update `README.md` in the same change.
- Always add or update automated tests for behavior changes and bug fixes, including regression coverage that would fail without the change.


================================================
FILE: CHANGELOG.md
================================================
# Changelog

## v0.5.1

[compare changes](https://github.com/unjs/httpxy/compare/v0.5.0...v0.5.1)

### 🚀 Enhancements

- **server:** `listeningCallback` ([#127](https://github.com/unjs/httpxy/pull/127))

### 🩹 Fixes

- Use `agents: false` for ws upgrade ([#129](https://github.com/unjs/httpxy/pull/129))
- Do not set undefined values for headers ([#130](https://github.com/unjs/httpxy/pull/130))

### 🏡 Chore

- Apply automated updates ([b227f65](https://github.com/unjs/httpxy/commit/b227f65))
- Update deps ([6f5ef12](https://github.com/unjs/httpxy/commit/6f5ef12))

### ❤️ Contributors

- Sukka <isukkaw@gmail.com>
- Daniel Roe ([@danielroe](https://github.com/danielroe))
- Pooya Parsa ([@pi0](https://github.com/pi0))

## v0.5.0

[compare changes](https://github.com/unjs/httpxy/compare/v0.4.0...v0.5.0)

### 🔥 Performance

- ⚠️ Keep-alive, request body stream and faster proxyFetch ([#124](https://github.com/unjs/httpxy/pull/124))

### 🏡 Chore

- Apply automated updates ([04aaaba](https://github.com/unjs/httpxy/commit/04aaaba))

#### ⚠️ Breaking Changes

- ⚠️ Keep-alive, request body stream and faster proxyFetch ([#124](https://github.com/unjs/httpxy/pull/124))

### ❤️ Contributors

- Pooya Parsa ([@pi0](https://github.com/pi0))

## v0.4.0

[compare changes](https://github.com/unjs/httpxy/compare/v0.3.1...v0.4.0)

### 🚀 Enhancements

- Http/2 listener support ([#102](https://github.com/unjs/httpxy/pull/102))
- **fetch:** Add proxyFetch options for timeout, xfwd, changeOrigin, agent, followRedirects, HTTPS, and path merging ([efa9711](https://github.com/unjs/httpxy/commit/efa9711))

### 🩹 Fixes

- **web-incoming:** Close downstream stream when upstream SSE aborts ([#103](https://github.com/unjs/httpxy/pull/103))
- Handle relative Location URLs in redirect rewriting ([#20](https://github.com/unjs/httpxy/pull/20), [#104](https://github.com/unjs/httpxy/pull/104))
- **web-outgoing:** Handle invalid response header characters gracefully ([#106](https://github.com/unjs/httpxy/pull/106))
- **web-incoming:** Remove deprecated `req.abort()` and `req.on("aborted")` ([#107](https://github.com/unjs/httpxy/pull/107))
- **web-outgoing:** Handle object target in redirect host rewrite ([#108](https://github.com/unjs/httpxy/pull/108))
- **web-incoming:** Remove deprecated `req.on('aborted')` listener ([#110](https://github.com/unjs/httpxy/pull/110))
- **ws:** Skip writing to closed socket on non-upgrade response ([#114](https://github.com/unjs/httpxy/pull/114))
- **web-incoming:** Guard `req.socket` access in error handler ([#112](https://github.com/unjs/httpxy/pull/112))
- **web-incoming:** Defer pipe until socket connects ([#111](https://github.com/unjs/httpxy/pull/111))
- **server:** Catch synchronous exceptions in middleware passes ([#109](https://github.com/unjs/httpxy/pull/109))
- **web-incoming:** Emit econnreset on client disconnect ([#115](https://github.com/unjs/httpxy/pull/115))
- **ws:** Handle response stream errors on failed WS upgrade ([#116](https://github.com/unjs/httpxy/pull/116))
- **web-outgoing:** Include HTTP 303 in redirect location rewriting ([#119](https://github.com/unjs/httpxy/pull/119))
- **web-outgoing:** Skip empty header names ([#121](https://github.com/unjs/httpxy/pull/121))
- **ssl:** Prevent undefined target values from overwriting ssl options ([#118](https://github.com/unjs/httpxy/pull/118))
- **utils:** Preserve target URL query string in path merging ([#117](https://github.com/unjs/httpxy/pull/117))
- **middleware:** Do not append duplicate x-forwarded-\* header values ([#120](https://github.com/unjs/httpxy/pull/120))
- **web-outgoing:** Strip transfer-encoding on 204/304 ([#122](https://github.com/unjs/httpxy/pull/122))
- **web-incoming:** Use `isSSL` regex for consistent https/wss protocol checks ([#123](https://github.com/unjs/httpxy/pull/123))
- **ws:** Preserve wss:// protocol and fix error handling in proxyUpgrade ([cb01605](https://github.com/unjs/httpxy/commit/cb01605))

### 📦 Build

- ⚠️ Esm-only ([d65b3f7](https://github.com/unjs/httpxy/commit/d65b3f7))

### 🏡 Chore

- Update deps ([743098d](https://github.com/unjs/httpxy/commit/743098d))

#### ⚠️ Breaking Changes

- ⚠️ Esm-only ([d65b3f7](https://github.com/unjs/httpxy/commit/d65b3f7))

### ❤️ Contributors

- Pooya Parsa ([@pi0](https://github.com/pi0))
- Guoyangzhen <upgyz@qq.com>
- Sukka <isukkaw@gmail.com>
- Gabor Koos <gabor.koos@gmail.com>

## v0.3.1

[compare changes](https://github.com/unjs/httpxy/compare/v0.3.0...v0.3.1)

### 🚀 Enhancements

- Standalone `proxyUpgrade` util ([#100](https://github.com/unjs/httpxy/pull/100))

### 🏡 Chore

- Apply automated updates ([d8c97ee](https://github.com/unjs/httpxy/commit/d8c97ee))

### ✅ Tests

- Use stub objects ([2287e56](https://github.com/unjs/httpxy/commit/2287e56))

### ❤️ Contributors

- Pooya Parsa ([@pi0](https://github.com/pi0))

## v0.3.0

[compare changes](https://github.com/unjs/httpxy/compare/v0.2.2...v0.3.0)

### 🚀 Enhancements

- `proxyFetch` ([#98](https://github.com/unjs/httpxy/pull/98))
- **web-incoming:** Implement native `followRedirects` support ([d3d7f39](https://github.com/unjs/httpxy/commit/d3d7f39))

### 🩹 Fixes

- **proxy:** Ensure leading slash on `toProxy` outgoing path ([7759c94](https://github.com/unjs/httpxy/commit/7759c94))
- **server:** Emit proxy error when listener exists, reject only when unhandled ([c9d2c51](https://github.com/unjs/httpxy/commit/c9d2c51))
- **web-incoming:** Destroy request socket on timeout ([40105be](https://github.com/unjs/httpxy/commit/40105be))
- **utils:** Preserve multiple consecutive slashes in request URL ([18e4d0d](https://github.com/unjs/httpxy/commit/18e4d0d))
- **web-incoming:** Abort proxy request when client disconnects ([a5d4996](https://github.com/unjs/httpxy/commit/a5d4996))
- **ws:** Handle client socket errors before upstream upgrade ([aebb5c6](https://github.com/unjs/httpxy/commit/aebb5c6))

### 💅 Refactors

- ⚠️ Remove legacy node `Url` support ([b2e6c92](https://github.com/unjs/httpxy/commit/b2e6c92))

### 🏡 Chore

- Enable strict typescript with nodenext resolution ([0c147a3](https://github.com/unjs/httpxy/commit/0c147a3))
- Format repo ([d7e707f](https://github.com/unjs/httpxy/commit/d7e707f))
- Update readme ([24f8b1a](https://github.com/unjs/httpxy/commit/24f8b1a))
- Add more examples for proxy fetch ([d0cb298](https://github.com/unjs/httpxy/commit/d0cb298))
- Apply automated updates ([d666b65](https://github.com/unjs/httpxy/commit/d666b65))
- Add agents.md ([f497cb0](https://github.com/unjs/httpxy/commit/f497cb0))
- Apply automated updates ([9a8d8eb](https://github.com/unjs/httpxy/commit/9a8d8eb))
- Apply automated updates ([822a0ea](https://github.com/unjs/httpxy/commit/822a0ea))
- Lint ([2d556f9](https://github.com/unjs/httpxy/commit/2d556f9))
- Update deps ([63b750f](https://github.com/unjs/httpxy/commit/63b750f))

### ✅ Tests

- Fix todo items ([8a3732b](https://github.com/unjs/httpxy/commit/8a3732b))
- Increase coverage ([50c0929](https://github.com/unjs/httpxy/commit/50c0929))
- Use random ports only ([9e2d155](https://github.com/unjs/httpxy/commit/9e2d155))

### 🤖 CI

- Update actions ([1fbac92](https://github.com/unjs/httpxy/commit/1fbac92))

#### ⚠️ Breaking Changes

- ⚠️ Remove legacy node `Url` support ([b2e6c92](https://github.com/unjs/httpxy/commit/b2e6c92))

### ❤️ Contributors

- Pooya Parsa ([@pi0](https://github.com/pi0))

## v0.2.2

[compare changes](https://github.com/unjs/httpxy/compare/v0.2.1...v0.2.2)

### 🏡 Chore

- Fix build script ([28dc9e6](https://github.com/unjs/httpxy/commit/28dc9e6))

### ❤️ Contributors

- Pooya Parsa ([@pi0](https://github.com/pi0))

## v0.2.1

[compare changes](https://github.com/unjs/httpxy/compare/v0.2.0...v0.2.1)

### 🌊 Types

- Make httpxy's server event type map generic ([#97](https://github.com/unjs/httpxy/pull/97))

### 🏡 Chore

- Update deps ([aecbed3](https://github.com/unjs/httpxy/commit/aecbed3))

### ❤️ Contributors

- Pooya Parsa ([@pi0](https://github.com/pi0))
- Sukka <isukkaw@gmail.com>

## v0.2.0

[compare changes](https://github.com/unjs/httpxy/compare/v0.1.7...v0.2.0)

### 💅 Refactors

- ⚠️ Code improvements ([#78](https://github.com/unjs/httpxy/pull/78))

### 🌊 Types

- Implement typed proxy server event ([#95](https://github.com/unjs/httpxy/pull/95), [#96](https://github.com/unjs/httpxy/pull/96))

### 🏡 Chore

- Update dev dependencies ([81f5e57](https://github.com/unjs/httpxy/commit/81f5e57))
- Migrate to oxfmt and oxlint ([edd6cff](https://github.com/unjs/httpxy/commit/edd6cff))

### ✅ Tests

- Port tests from node-http-proxy ([#88](https://github.com/unjs/httpxy/pull/88))

#### ⚠️ Breaking Changes

- ⚠️ Code improvements ([#78](https://github.com/unjs/httpxy/pull/78))

### ❤️ Contributors

- Pooya Parsa ([@pi0](https://github.com/pi0))
- Sukka ([@SukkaW](https://github.com/SukkaW))
- 翠 <green@sapphi.red>

## v0.1.7

[compare changes](https://github.com/unjs/httpxy/compare/v0.1.6...v0.1.7)

### 🩹 Fixes

- Preserve double slashes in url ([#70](https://github.com/unjs/httpxy/pull/70))

### 🏡 Chore

- Update deps ([c9c9de8](https://github.com/unjs/httpxy/commit/c9c9de8))

### ❤️ Contributors

- Oskar Lebuda ([@OskarLebuda](http://github.com/OskarLebuda))
- Pooya Parsa ([@pi0](http://github.com/pi0))

## v0.1.6

[compare changes](https://github.com/unjs/httpxy/compare/v0.1.5...v0.1.6)

### 🩹 Fixes

- Omit outgoing port when not required ([#65](https://github.com/unjs/httpxy/pull/65))

### 📖 Documentation

- Remove unsupported `followRedirects` option ([#66](https://github.com/unjs/httpxy/pull/66))
- Improve example ([#16](https://github.com/unjs/httpxy/pull/16))

### 🏡 Chore

- Fix typo in readme ([#36](https://github.com/unjs/httpxy/pull/36))
- Update repo ([64f7465](https://github.com/unjs/httpxy/commit/64f7465))
- Update ci ([b0f08de](https://github.com/unjs/httpxy/commit/b0f08de))

### ❤️ Contributors

- Lsh ([@peterroe](http://github.com/peterroe))
- Kricsleo ([@kricsleo](http://github.com/kricsleo))
- Pooya Parsa ([@pi0](http://github.com/pi0))
- Mohammd Siddiqui <masiddiqui91@gmail.com>

## v0.1.5

[compare changes](https://github.com/unjs/httpxy/compare/v0.1.4...v0.1.5)

### 🩹 Fixes

- Handle client `close` event ([#8](https://github.com/unjs/httpxy/pull/8))

### 🏡 Chore

- Update deps ([2888089](https://github.com/unjs/httpxy/commit/2888089))

### ❤️ Contributors

- Pooya Parsa ([@pi0](http://github.com/pi0))
- David Tai ([@didavid61202](http://github.com/didavid61202))

## v0.1.4

[compare changes](https://github.com/unjs/httpxy/compare/v0.1.2...v0.1.4)

### 🩹 Fixes

- Presrve search params from parsed url ([8bbaacc](https://github.com/unjs/httpxy/commit/8bbaacc))
- Add `target` pathname currectly ([#6](https://github.com/unjs/httpxy/pull/6))

### 💅 Refactors

- Fix typo in `defineProxyMiddleware` ([#4](https://github.com/unjs/httpxy/pull/4))

### 🏡 Chore

- **release:** V0.1.2 ([b6bd4a8](https://github.com/unjs/httpxy/commit/b6bd4a8))
- Update dev dependencies ([5704e70](https://github.com/unjs/httpxy/commit/5704e70))
- **release:** V0.1.3 ([4ced1cc](https://github.com/unjs/httpxy/commit/4ced1cc))

### ❤️ Contributors

- Pooya Parsa ([@pi0](http://github.com/pi0))
- Jonasolesen
- Gacek1123

## v0.1.3

[compare changes](https://github.com/unjs/httpxy/compare/v0.1.2...v0.1.3)

### 🩹 Fixes

- Presrve search params from parsed url ([8bbaacc](https://github.com/unjs/httpxy/commit/8bbaacc))

### 💅 Refactors

- Fix typo in `defineProxyMiddleware` ([#4](https://github.com/unjs/httpxy/pull/4))

### 🏡 Chore

- **release:** V0.1.2 ([b6bd4a8](https://github.com/unjs/httpxy/commit/b6bd4a8))
- Update dev dependencies ([a41d0c6](https://github.com/unjs/httpxy/commit/a41d0c6))

### ❤️ Contributors

- Pooya Parsa ([@pi0](http://github.com/pi0))
- Gacek1123

## v0.1.2

[compare changes](https://github.com/unjs/httpxy/compare/v0.1.1...v0.1.2)

### 🩹 Fixes

- Presrve search params from parsed url ([8bbaacc](https://github.com/unjs/httpxy/commit/8bbaacc))

### ❤️ Contributors

- Pooya Parsa ([@pi0](http://github.com/pi0))

## v0.1.1

### 🚀 Enhancements

- Awaitable `.`web`and`.ws` ([e4dad27](https://github.com/unjs/httpxy/commit/e4dad27))

### 🩹 Fixes

- `createProxyServer` options is optional ([75d8e93](https://github.com/unjs/httpxy/commit/75d8e93))

### 💅 Refactors

- Avoid `url.parse` ([4ceca85](https://github.com/unjs/httpxy/commit/4ceca85))
- Hide internal props ([2f30878](https://github.com/unjs/httpxy/commit/2f30878))

### 📖 Documentation

- No need for quote ([9319fab](https://github.com/unjs/httpxy/commit/9319fab))

### 🏡 Chore

- Update readme ([64a7a75](https://github.com/unjs/httpxy/commit/64a7a75))
- Update dependencies ([1e906b9](https://github.com/unjs/httpxy/commit/1e906b9))

### ❤️ Contributors

- Pooya Parsa ([@pi0](http://github.com/pi0))
- Sébastien Chopin <seb@nuxtlabs.com>


================================================
FILE: CLAUDE.md
================================================
@AGENTS.md


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) Pooya Parsa <pooya@pi0.io>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

----

Based on http-party/node-http-proxy (9b96cd7)

Copyright (c) 2010-2016 Charlie Robbins, Jarrett Cruger & the Contributors.

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
# 🔀 httpxy

[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
[![bundle][bundle-src]][bundle-href]
[![Codecov][codecov-src]][codecov-href]

A Full-Featured HTTP and WebSocket Proxy for Node.js

## Proxy Fetch

`proxyFetch` is a proxy utility with web standard (`Request`/`Response`) interfaces. It forwards requests to a specific server address (TCP host/port or Unix socket), bypassing the URL's hostname.

```ts
import { proxyFetch } from "httpxy";

// TCP — using a URL string
const res = await proxyFetch("http://127.0.0.1:3000", "http://example.com/api/data");
console.log(await res.json());

// Unix socket — using a URL string
const res2 = await proxyFetch("unix:/tmp/app.sock", "http://localhost/health");
console.log(await res2.text());

// Or use an object for more control
const res3 = await proxyFetch({ host: "127.0.0.1", port: 3000 }, "http://example.com/api/data");

// Using a Request object
const req = new Request("http://example.com/api/data", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ key: "value" }),
});
const res4 = await proxyFetch("http://127.0.0.1:3000", req);

// Using a URL string with RequestInit
const res5 = await proxyFetch("http://127.0.0.1:3000", "http://example.com/api/data", {
  method: "PUT",
  headers: { Authorization: "Bearer token" },
  body: JSON.stringify({ updated: true }),
});
```

It accepts the same `input` and `init` arguments as the global `fetch`, including `Request` objects and streaming bodies, and returns a standard `Response`. Redirects are handled manually by default.

## Proxy Upgrade

`proxyUpgrade` is a standalone WebSocket upgrade proxy. It forwards `upgrade` requests to a target server without needing a `ProxyServer` instance — the WebSocket counterpart to `proxyFetch`.

```ts
import { createServer } from "node:http";
import { proxyUpgrade } from "httpxy";

const server = createServer((req, res) => {
  // Handle regular HTTP requests...
});

server.on("upgrade", (req, socket, head) => {
  proxyUpgrade("http://127.0.0.1:8080", req, socket, head);
});

server.listen(3000);
```

It accepts the same `addr` formats as `proxyFetch` (`"http://host:port"`, `"unix:/path"`, or `{ host, port }` / `{ socketPath }`), and returns a `Promise<Socket>` that resolves with the upstream proxy socket once the WebSocket connection is established.

```ts
// With options
server.on("upgrade", (req, socket, head) => {
  proxyUpgrade({ host: "127.0.0.1", port: 8080 }, req, socket, head, {
    // changeOrigin: true, // rewrite Host header
    // xfwd: false, // disable x-forwarded-* headers (enabled by default)
  });
});
```

## Proxy Server

> [!NOTE]
> Proxy server was originally forked from [http-party/node-http-proxy](https://github.com/http-party/node-http-proxy).

Create proxy:

```ts
import { createServer } from "node:http";
import { createProxyServer } from "httpxy";

const proxy = createProxyServer({});

const server = createServer(async (req, res) => {
  try {
    await proxy.web(req, res, {
      target: address /* address of your proxy server here */,
    });
  } catch (error) {
    console.error(error);
    res.statusCode = 500;
    res.end("Proxy error: " + error.toString());
  }
});

server.listen(3000, () => {
  console.log("Proxy is listening on http://localhost:3000");
});
```

## Options

| Option                  | Type                                   | Default    | Description                                                                 |
| ----------------------- | -------------------------------------- | ---------- | --------------------------------------------------------------------------- |
| `target`                | `string \| URL \| ProxyTargetDetailed` | —          | Target server URL                                                           |
| `forward`               | `string \| URL`                        | —          | Forward server URL (pipes request without the target's response)            |
| `agent`                 | `http.Agent \| false`                  | keep-alive | Shared keep-alive agent by default. Set `false` to disable connection reuse |
| `ssl`                   | `https.ServerOptions`                  | —          | Object passed to `https.createServer()`                                     |
| `ws`                    | `boolean`                              | `false`    | Enable WebSocket proxying                                                   |
| `xfwd`                  | `boolean`                              | `false`    | Add `x-forwarded-*` headers                                                 |
| `secure`                | `boolean`                              | —          | Verify SSL certificates                                                     |
| `toProxy`               | `boolean`                              | `false`    | Pass absolute URL as path (proxy-to-proxy)                                  |
| `prependPath`           | `boolean`                              | `true`     | Prepend the target's path to the proxy path                                 |
| `ignorePath`            | `boolean`                              | `false`    | Ignore the incoming request path                                            |
| `localAddress`          | `string`                               | —          | Local interface to bind for outgoing connections                            |
| `changeOrigin`          | `boolean`                              | `false`    | Change the `Host` header to match the target URL                            |
| `preserveHeaderKeyCase` | `boolean`                              | `false`    | Keep original letter case of response header keys                           |
| `auth`                  | `string`                               | —          | Basic authentication (`'user:password'`) for `Authorization` header         |
| `hostRewrite`           | `string`                               | —          | Rewrite the `Location` hostname on redirects (301/302/307/308)              |
| `autoRewrite`           | `boolean`                              | `false`    | Rewrite `Location` host/port on redirects based on the request              |
| `protocolRewrite`       | `string`                               | —          | Rewrite `Location` protocol on redirects (`'http'` or `'https'`)            |
| `cookieDomainRewrite`   | `false \| string \| object`            | `false`    | Rewrite domain of `Set-Cookie` headers                                      |
| `cookiePathRewrite`     | `false \| string \| object`            | `false`    | Rewrite path of `Set-Cookie` headers                                        |
| `headers`               | `object`                               | —          | Extra headers to add to target requests                                     |
| `proxyTimeout`          | `number`                               | `120000`   | Timeout (ms) for the proxy request to the target                            |
| `timeout`               | `number`                               | —          | Timeout (ms) for the incoming request                                       |
| `selfHandleResponse`    | `boolean`                              | `false`    | Disable automatic response piping (handle `proxyRes` yourself)              |
| `followRedirects`       | `boolean \| number`                    | `false`    | Follow HTTP redirects from target. `true` = max 5 hops; number = custom max |
| `buffer`                | `stream.Stream`                        | —          | Stream to use as request body instead of the incoming request               |

## Events

| Event        | Arguments                                | Description                                            |
| ------------ | ---------------------------------------- | ------------------------------------------------------ |
| `error`      | `(err, req, res, target)`                | An error occurred during proxying                      |
| `proxyReq`   | `(proxyReq, req, res, options)`          | Before request is sent to target (modify headers here) |
| `proxyRes`   | `(proxyRes, req, res)`                   | Response received from target                          |
| `proxyReqWs` | `(proxyReq, req, socket, options, head)` | Before WebSocket upgrade request is sent               |
| `open`       | `(proxySocket)`                          | WebSocket connection opened                            |
| `close`      | `(proxyRes, proxySocket, proxyHead)`     | WebSocket connection closed                            |
| `start`      | `(req, res, target)`                     | Proxy processing started                               |
| `end`        | `(req, res, proxyRes)`                   | Proxy request completed                                |

## Examples

### HTTP Proxy

```ts
import { createServer } from "node:http";
import { createProxyServer } from "httpxy";

const proxy = createProxyServer({});

const server = createServer(async (req, res) => {
  await proxy.web(req, res, { target: "http://localhost:8080" });
});

server.listen(3000);
```

### WebSocket Proxy

```ts
import { createServer } from "node:http";
import { createProxyServer } from "httpxy";

const proxy = createProxyServer({ target: "http://localhost:8080", ws: true });

const server = createServer(async (req, res) => {
  await proxy.web(req, res);
});

server.on("upgrade", (req, socket, head) => {
  proxy.ws(req, socket, { target: "http://localhost:8080" }, head);
});

server.listen(3000);
```

### Modify Request Headers

```ts
import { createServer } from "node:http";
import { createProxyServer } from "httpxy";

const proxy = createProxyServer({ target: "http://localhost:8080" });

proxy.on("proxyReq", (proxyReq) => {
  proxyReq.setHeader("X-Forwarded-By", "httpxy");
});

const server = createServer(async (req, res) => {
  await proxy.web(req, res);
});

server.listen(3000);
```

### HTTPS Proxy

```ts
import { readFileSync } from "node:fs";
import { createProxyServer } from "httpxy";

const proxy = createProxyServer({
  ssl: {
    key: readFileSync("server-key.pem", "utf8"),
    cert: readFileSync("server-cert.pem", "utf8"),
  },
  target: "https://localhost:8443",
  secure: false, // allow self-signed certificates
});

proxy.listen(3000);
```

### Standalone Proxy Server

```ts
import { createProxyServer } from "httpxy";

const proxy = createProxyServer({
  target: "http://localhost:8080",
  changeOrigin: true,
});

proxy.listen(3000);
```

## Development

- Clone this repository
- Install latest LTS version of [Node.js](https://nodejs.org/en/)
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
- Install dependencies using `pnpm install`
- Run interactive tests using `pnpm dev`

## Acknowledgements

Performance optimizations in httpxy were inspired by analysis of [fast-proxy](https://github.com/fastify/fast-proxy) and [@fastify/http-proxy](https://github.com/fastify/fastify-http-proxy).

## License

Made with 💛

Published under [MIT License](./LICENSE).

<!-- Badges -->

[npm-version-src]: https://img.shields.io/npm/v/httpxy?style=flat&colorA=18181B&colorB=F0DB4F
[npm-version-href]: https://npmjs.com/package/httpxy
[npm-downloads-src]: https://img.shields.io/npm/dm/httpxy?style=flat&colorA=18181B&colorB=F0DB4F
[npm-downloads-href]: https://npmjs.com/package/httpxy
[codecov-src]: https://img.shields.io/codecov/c/gh/unjs/httpxy/main?style=flat&colorA=18181B&colorB=F0DB4F
[codecov-href]: https://codecov.io/gh/unjs/httpxy
[bundle-src]: https://img.shields.io/bundlephobia/minzip/httpxy?style=flat&colorA=18181B&colorB=F0DB4F
[bundle-href]: https://bundlephobia.com/result?p=httpxy


================================================
FILE: bench/Dockerfile
================================================
FROM node:lts

COPY --from=alpine/bombardier /usr/bin/bombardier /usr/local/bin/bombardier
RUN corepack enable && corepack prepare pnpm@latest --activate

WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY bench/package.json bench/
RUN pnpm install --frozen-lockfile

COPY src/ src/
COPY bench/ bench/
COPY tsconfig.json ./


================================================
FILE: bench/bench.ts
================================================
#!/usr/bin/env node

import { execSync, execFileSync, execFile as _execFile } from "node:child_process";
import { parseArgs } from "node:util";
import { promisify } from "node:util";

const execFileAsync = promisify(_execFile);

// --- Config ---

const { values: args } = parseArgs({
  options: {
    duration: { type: "string", short: "d", default: "1s" },
    connections: { type: "string", short: "c", default: "128" },
    sequential: { type: "boolean", short: "s", default: true },
  },
});

const IMAGE = "httpxy-bench";
const DURATION = args.duration!;
const CONNECTIONS = Number(args.connections);
const SEQUENTIAL = args.sequential!;
const POST_BODY = JSON.stringify({
  message: "hello world".repeat(30),
  ts: 1234567890,
  padding: "x".repeat(1024 - 360),
}); // ~1KB
const TARGET_PORT = 3000;

const PROXIES = [
  { name: "httpxy.server", script: "bench/src/httpxy-server.ts", port: 3001 },
  { name: "httpxy.proxyFetch", script: "bench/src/httpxy-fetch.ts", port: 3002 },
  { name: "fast-proxy", script: "bench/src/fast-proxy.ts", port: 3003 },
  { name: "@fastify/http-proxy", script: "bench/src/fastify.ts", port: 3004 },
  { name: "http-proxy-3", script: "bench/src/http-proxy-3.ts", port: 3005 },
  { name: "http-proxy", script: "bench/src/http-proxy.ts", port: 3006 },
];

// --- Helpers ---

const blue = (s: string) => `\x1B[1;34m${s}\x1B[0m`;
const green = (s: string) => `\x1B[1;32m${s}\x1B[0m`;
// const red = (s: string) => `\x1B[1;31m${s}\x1B[0m`;

const info = (msg: string) => console.log(blue(`=> ${msg}`));
const ok = (msg: string) => console.log(green(`   ${msg}`));
// const err = (msg: string) => console.log(red(`   ${msg}`));

const containers: string[] = [];

function cleanup() {
  info("Cleaning up...");
  if (containers.length === 0) return;
  try {
    execSync(`docker rm -f ${containers.join(" ")}`, { stdio: "ignore" });
  } catch {}
  containers.length = 0;
}

function dockerRun(...args: string[]) {
  return execFileSync("docker", ["run", "--rm", "--network", "host", ...args], {
    encoding: "utf8",
  }).trim();
}

function startContainer(name: string, script: string, port: number) {
  const cid = dockerRun(
    "-d",
    "--name",
    name,
    "--cpus=1",
    "--memory=256m",
    "-e",
    `PORT=${port}`,
    "-e",
    `TARGET=http://127.0.0.1:${TARGET_PORT}`,
    IMAGE,
    "node",
    script,
  );
  containers.push(cid);
}

let bombCounter = 0;

async function bombJson(args: string[]): Promise<string> {
  const name = `bench-bombardier-${process.pid}-${bombCounter++}`;
  const { stdout } = await execFileAsync(
    "docker",
    [
      "run",
      "--rm",
      "--network",
      "host",
      "--name",
      name,
      IMAGE,
      "bombardier",
      "--format=json",
      "--print=result",
      "--latencies",
      ...args,
    ],
    { encoding: "utf8" },
  );
  return stdout;
}

function waitForReady(port: number, retries = 60) {
  for (let i = 0; i < retries; i++) {
    try {
      execSync(`curl -sf -o /dev/null http://127.0.0.1:${port}/`, { stdio: "ignore" });
      return;
    } catch {
      execSync("sleep 0.3", { stdio: "ignore" });
    }
  }
  throw new Error(`Timed out waiting for port ${port}`);
}

// --- Bombardier types ---

interface BombardierResult {
  bytesRead: number;
  bytesWritten: number;
  timeTakenSeconds: number;
  req1xx: number;
  req2xx: number;
  req3xx: number;
  req4xx: number;
  req5xx: number;
  others: number;
  errors?: { description: string; count: number }[];
  latency: {
    mean: number; // nanoseconds
    stddev: number;
    max: number;
    percentiles: Record<string, number>; // "50", "75", "90", "95", "99"
  };
  rps: {
    mean: number;
    stddev: number;
    max: number;
    percentiles: Record<string, number>;
  };
}

interface BenchResult {
  rps: number;
  avgLatency: number; // ns
  p50: number; // ns
  p99: number; // ns
  bytesPerSec: number;
}

// --- Result parsing ---

function formatNs(ns: number): string {
  return ns < 1e6 ? `${(ns / 1e3).toFixed(0)}µs` : `${(ns / 1e6).toFixed(2)}ms`;
}

function formatThroughput(bytesPerSec: number): string {
  return bytesPerSec > 1e6
    ? `${(bytesPerSec / 1e6).toFixed(1)}MB/s`
    : `${(bytesPerSec / 1e3).toFixed(0)}KB/s`;
}

function formatResult(r: BenchResult): string {
  return `${r.rps.toFixed(0)} req/s | ${formatNs(r.avgLatency)} avg | ${formatThroughput(r.bytesPerSec)}`;
}

function parseResult(json: string): BenchResult {
  const { result: r } = JSON.parse(json) as { result: BombardierResult };

  const nonOk = r.req1xx + r.req3xx + r.req4xx + r.req5xx + r.others;
  if (nonOk > 0) {
    throw new Error(
      `Non-2xx responses: 1xx=${r.req1xx} 3xx=${r.req3xx} 4xx=${r.req4xx} 5xx=${r.req5xx} other=${r.others}`,
    );
  }
  if (r.errors && r.errors.length > 0) {
    const details = r.errors.map((e) => `${e.description}(${e.count})`).join(", ");
    throw new Error(`Transport errors: ${details}`);
  }

  return {
    rps: r.rps.mean,
    avgLatency: r.latency.mean,
    p50: r.latency.percentiles["50"] ?? 0,
    p99: r.latency.percentiles["99"] ?? 0,
    bytesPerSec: r.bytesRead / r.timeTakenSeconds,
  };
}

const HEADERS = ["Proxy", "Req/s", "Scale", "Avg", "P50", "P99", "Throughput"];

function printTable(title: string, results: [name: string, result: BenchResult][]) {
  // Sort by req/s descending
  results.sort((a, b) => b[1].rps - a[1].rps);
  const bestRps = Math.max(...results.map(([, r]) => r.rps));

  // Build rows
  const rows = results.map(([name, r]) => {
    const ratio = bestRps > 0 ? r.rps / bestRps : 0;
    return [
      name,
      r.rps.toFixed(0),
      ratio >= 1 ? "1.00x" : `${ratio.toFixed(2)}x`,
      formatNs(r.avgLatency),
      formatNs(r.p50),
      formatNs(r.p99),
      formatThroughput(r.bytesPerSec),
    ];
  });

  // Compute column widths from headers + data
  const colWidths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i]!.length)));

  const mdRow = (cells: string[]) =>
    `| ${cells.map((c, i) => (i === 0 ? c.padEnd(colWidths[i]!) : c.padStart(colWidths[i]!))).join(" | ")} |`;

  console.log();
  console.log(`### ${title}`);
  console.log();
  console.log(mdRow(HEADERS));
  console.log(
    `| ${colWidths.map((w, i) => (i === 0 ? `:${"-".repeat(w - 1)}` : `${"-".repeat(w - 1)}:`)).join(" | ")} |`,
  );
  for (const row of rows) {
    console.log(mdRow(row));
  }
}

// --- Main ---

process.on("exit", cleanup);
process.on("SIGINT", () => process.exit(1));
process.on("SIGTERM", () => process.exit(1));

info("Running validation tests...");
execSync("node bench/test.ts", {
  stdio: "inherit",
  cwd: `${import.meta.dirname}/..`,
});
ok("All implementations valid");

info("Building image...");
execSync(`docker build -t ${IMAGE} -f bench/Dockerfile .`, {
  stdio: "inherit",
  cwd: `${import.meta.dirname}/..`,
});

info("Starting target server...");
startContainer("bench-target", "bench/src/target.ts", TARGET_PORT);
waitForReady(TARGET_PORT);
ok("target ready");

info("Starting proxy servers...");
for (const { name, script, port } of PROXIES) {
  const containerName = `bench-${name.replaceAll(" ", "-").replaceAll(/[@/]/g, "")}`;
  startContainer(containerName, script, port);
}

for (const { name, port } of PROXIES) {
  waitForReady(port);
  ok(`${name} ready`);
}
console.log();

async function runBench(label: string, extraArgs: string[] = []) {
  info(
    `Benchmarking ${label} (duration=${DURATION}, connections=${CONNECTIONS}${SEQUENTIAL ? ", sequential" : ""})`,
  );
  const benchOne = async ({ name, port }: (typeof PROXIES)[number]) => {
    const json = await bombJson([
      "-c",
      String(CONNECTIONS),
      "-d",
      DURATION,
      ...extraArgs,
      `http://127.0.0.1:${port}/`,
    ]);
    const result = parseResult(json);
    ok(`${name} — ${formatResult(result)}`);
    return [name, result] as [string, BenchResult];
  };
  let results: [string, BenchResult][];
  if (SEQUENTIAL) {
    results = [];
    for (const proxy of PROXIES) {
      results.push(await benchOne(proxy));
    }
  } else {
    results = await Promise.all(PROXIES.map(benchOne));
  }
  console.log();
  return results;
}

const getResults = await runBench("GET");
const postResults = await runBench("POST ~1KB JSON", [
  "-m",
  "POST",
  "-H",
  "Content-Type: application/json",
  "-b",
  POST_BODY,
]);

// --- Summary ---

console.log();
info("Summary");
console.log();
console.log(
  `> Duration: **${DURATION}** | Connections: **${CONNECTIONS}** | Mode: **${SEQUENTIAL ? "sequential" : "parallel"}**`,
);

printTable("GET (no body)", getResults);
console.log();
printTable("POST (~1KB JSON)", postResults);
console.log();
info("Done!");


================================================
FILE: bench/package.json
================================================
{
  "name": "bench",
  "private": true,
  "type": "module",
  "devDependencies": {
    "@fastify/http-proxy": "^11.4.2",
    "@types/http-proxy": "^1.17.17",
    "fast-proxy": "^2.2.0",
    "fastify": "^5.8.4",
    "http-proxy": "^1.18.1",
    "http-proxy-3": "^1.23.2",
    "mitata": "^1.0.34"
  }
}


================================================
FILE: bench/src/fast-proxy.ts
================================================
import http from "node:http";
import fastProxy from "fast-proxy";

const PORT = Number(process.env.PORT) || 3003;
const TARGET = process.env.TARGET || "http://target:3000";

const { proxy } = fastProxy({ base: TARGET });
const server = http.createServer((req, res) => {
  proxy(req, res, req.url!, {});
});

server.listen(PORT, "0.0.0.0", () => {
  console.log(`fast-proxy listening on :${PORT} -> ${TARGET}`);
});


================================================
FILE: bench/src/fastify.ts
================================================
import Fastify from "fastify";
import httpProxy from "@fastify/http-proxy";

const PORT = Number(process.env.PORT) || 3004;
const TARGET = process.env.TARGET || "http://target:3000";

const app = Fastify();
await app.register(httpProxy, { upstream: TARGET });
await app.listen({ port: PORT, host: "0.0.0.0" });
console.log(`@fastify/http-proxy listening on :${PORT} -> ${TARGET}`);


================================================
FILE: bench/src/http-proxy-3.ts
================================================
import http from "node:http";
import { createProxyServer } from "http-proxy-3";

const PORT = Number(process.env.PORT) || 3005;
const TARGET = process.env.TARGET || "http://target:3000";

const proxy = createProxyServer({ target: TARGET });
const server = http.createServer((req, res) => {
  proxy.web(req, res);
});

server.listen(PORT, "0.0.0.0", () => {
  console.log(`http-proxy-3 listening on :${PORT} -> ${TARGET}`);
});


================================================
FILE: bench/src/http-proxy.ts
================================================
import http from "node:http";
import httpProxy from "http-proxy";

const PORT = Number(process.env.PORT) || 3006;
const TARGET = process.env.TARGET || "http://target:3000";

const proxy = httpProxy.createProxyServer({ target: TARGET });
const server = http.createServer((req, res) => {
  proxy.web(req, res);
});

server.listen(PORT, "0.0.0.0", () => {
  console.log(`http-proxy listening on :${PORT} -> ${TARGET}`);
});


================================================
FILE: bench/src/httpxy-fetch.ts
================================================
import http from "node:http";
import { proxyFetch } from "../../src/index.ts";

const PORT = Number(process.env.PORT) || 3002;
const TARGET = process.env.TARGET || "http://target:3000";

function collectBody(req: http.IncomingMessage): Promise<Buffer | undefined> {
  if (req.method === "GET" || req.method === "HEAD") {
    return Promise.resolve(undefined);
  }
  return new Promise((resolve) => {
    const chunks: Buffer[] = [];
    req.on("data", (c: Buffer) => chunks.push(c));
    req.on("end", () => resolve(chunks.length > 0 ? Buffer.concat(chunks) : undefined));
  });
}

const server = http.createServer(async (req, res) => {
  const body = await collectBody(req);
  const response = await proxyFetch(TARGET, new URL(req.url!, `http://127.0.0.1:${PORT}`), {
    method: req.method,
    headers: req.headers as HeadersInit,
    body: body as any,
  });
  res.writeHead(response.status, Object.fromEntries(response.headers));
  if (response.body) {
    for await (const chunk of response.body) {
      res.write(chunk);
    }
  }
  res.end();
});

server.listen(PORT, "0.0.0.0", () => {
  console.log(`httpxy proxyFetch proxy listening on :${PORT} -> ${TARGET}`);
});


================================================
FILE: bench/src/httpxy-server.ts
================================================
import http from "node:http";
import { createProxyServer } from "../../src/index.ts";

const PORT = Number(process.env.PORT) || 3001;
const TARGET = process.env.TARGET || "http://target:3000";

const proxy = createProxyServer({ target: TARGET });
const server = http.createServer((req, res) => {
  proxy.web(req, res);
});

server.listen(PORT, "0.0.0.0", () => {
  console.log(`httpxy server proxy listening on :${PORT} -> ${TARGET}`);
});


================================================
FILE: bench/src/target.ts
================================================
import http from "node:http";

const PORT = Number(process.env.PORT) || 3000;

const server = http.createServer((req, res) => {
  if (req.method === "GET") {
    res.writeHead(200, { "content-type": "application/json" });
    res.end('{"ok":true}');
    return;
  }
  const chunks: Buffer[] = [];
  req.on("data", (c) => chunks.push(c));
  req.on("end", () => {
    const body = Buffer.concat(chunks);
    res.writeHead(200, {
      "content-type": req.headers["content-type"] || "application/octet-stream",
      "content-length": String(body.length),
    });
    res.end(body);
  });
});

server.listen(PORT, "0.0.0.0", () => {
  console.log(`target listening on :${PORT}`);
});


================================================
FILE: bench/test.ts
================================================
#!/usr/bin/env node
import http from "node:http";
import { createProxyServer, proxyFetch } from "../src/index.ts";
import fastProxy from "fast-proxy";
import { createProxyServer as createHttpProxy3 } from "http-proxy-3";
import httpProxyLegacy from "http-proxy";
import Fastify from "fastify";
import httpProxy from "@fastify/http-proxy";

// --- Config ---

const TARGET_PORT = 9_900;
const HTTPXY_SERVER_PORT = 9_901;
const HTTPXY_FETCH_PORT = 9_902;
const FAST_PROXY_PORT = 9_903;
const FASTIFY_PROXY_PORT = 9_904;
const HTTP_PROXY_3_PORT = 9_905;
const HTTP_PROXY_PORT = 9_906;

const SMALL_BODY = JSON.stringify({ message: "hello world", ts: Date.now() });
const LARGE_BODY = JSON.stringify({
  data: Array.from({ length: 1000 }, (_, i) => ({
    id: i,
    name: `item-${i}`,
    value: Math.random(),
    tags: ["a", "b", "c"],
  })),
});

// --- Target server (echo) ---

function createTargetServer(): Promise<http.Server> {
  return new Promise((resolve) => {
    const server = http.createServer((req, res) => {
      if (req.method === "GET") {
        res.writeHead(200, { "content-type": "application/json" });
        res.end('{"ok":true}');
        return;
      }
      const chunks: Buffer[] = [];
      req.on("data", (c) => chunks.push(c));
      req.on("end", () => {
        res.writeHead(200, {
          "content-type": req.headers["content-type"] || "application/octet-stream",
          "content-length": String(Buffer.concat(chunks).length),
        });
        res.end(Buffer.concat(chunks));
      });
    });
    server.listen(TARGET_PORT, () => resolve(server));
  });
}

// --- Proxy servers setup ---

const TARGET = `http://127.0.0.1:${TARGET_PORT}`;

async function setupHttpxyServer(): Promise<http.Server> {
  const proxy = createProxyServer({ target: TARGET });
  const server = http.createServer((req, res) => {
    proxy.web(req, res);
  });
  return new Promise((resolve) => {
    server.listen(HTTPXY_SERVER_PORT, () => resolve(server));
  });
}

function collectBody(req: http.IncomingMessage): Promise<Buffer | undefined> {
  if (req.method === "GET" || req.method === "HEAD") {
    return Promise.resolve(undefined);
  }
  return new Promise((resolve) => {
    const chunks: Buffer[] = [];
    req.on("data", (c: Buffer) => chunks.push(c));
    req.on("end", () => resolve(chunks.length > 0 ? Buffer.concat(chunks) : undefined));
  });
}

async function setupHttpxyFetchServer(): Promise<http.Server> {
  const server = http.createServer(async (req, res) => {
    const body = await collectBody(req);
    const response = await proxyFetch(
      TARGET,
      new URL(req.url!, `http://127.0.0.1:${HTTPXY_FETCH_PORT}`),
      {
        method: req.method,
        headers: req.headers as HeadersInit,
        body: body as any,
      },
    );
    res.writeHead(response.status, Object.fromEntries(response.headers));
    if (response.body) {
      for await (const chunk of response.body) {
        res.write(chunk);
      }
    }
    res.end();
  });
  return new Promise((resolve) => {
    server.listen(HTTPXY_FETCH_PORT, () => resolve(server));
  });
}

async function setupFastProxy(): Promise<{ server: http.Server; close: () => void }> {
  const { proxy, close } = fastProxy({ base: TARGET });
  const server = http.createServer((req, res) => {
    proxy(req, res, req.url!, {});
  });
  return new Promise((resolve) => {
    server.listen(FAST_PROXY_PORT, () => resolve({ server, close }));
  });
}

async function setupFastifyProxy(): Promise<ReturnType<typeof Fastify>> {
  const app = Fastify();
  await app.register(httpProxy, { upstream: TARGET });
  await app.listen({ port: FASTIFY_PROXY_PORT });
  return app;
}

async function setupHttpProxy3(): Promise<http.Server> {
  const proxy = createHttpProxy3({ target: TARGET });
  const server = http.createServer((req, res) => {
    proxy.web(req, res);
  });
  return new Promise((resolve) => {
    server.listen(HTTP_PROXY_3_PORT, () => resolve(server));
  });
}

async function setupHttpProxyLegacy(): Promise<http.Server> {
  const proxy = httpProxyLegacy.createProxyServer({ target: TARGET });
  const server = http.createServer((req, res) => {
    proxy.web(req, res);
  });
  return new Promise((resolve) => {
    server.listen(HTTP_PROXY_PORT, () => resolve(server));
  });
}

// --- HTTP helpers ---

interface HttpResult {
  status: number;
  headers: http.IncomingHttpHeaders;
  body: string;
}

function httpGet(port: number, path = "/"): Promise<HttpResult> {
  return new Promise((resolve, reject) => {
    const req = http.get(`http://127.0.0.1:${port}${path}`, (res) => {
      const chunks: Buffer[] = [];
      res.on("data", (c) => chunks.push(c));
      res.on("end", () =>
        resolve({
          status: res.statusCode!,
          headers: res.headers,
          body: Buffer.concat(chunks).toString(),
        }),
      );
    });
    req.on("error", reject);
  });
}

function httpPost(port: number, body: string, path = "/"): Promise<HttpResult> {
  return new Promise((resolve, reject) => {
    const req = http.request(
      {
        hostname: "127.0.0.1",
        port,
        path,
        method: "POST",
        headers: {
          "content-type": "application/json",
          "content-length": Buffer.byteLength(body),
        },
      },
      (res) => {
        const chunks: Buffer[] = [];
        res.on("data", (c) => chunks.push(c));
        res.on("end", () =>
          resolve({
            status: res.statusCode!,
            headers: res.headers,
            body: Buffer.concat(chunks).toString(),
          }),
        );
      },
    );
    req.on("error", reject);
    req.end(body);
  });
}

// --- Main ---

async function main() {
  console.log("Starting servers...");

  const targetServer = await createTargetServer();
  const httpxyServer = await setupHttpxyServer();
  const httpxyFetchServer = await setupHttpxyFetchServer();
  const fastProxySetup = await setupFastProxy();
  const fastifyApp = await setupFastifyProxy();
  const httpProxy3Server = await setupHttpProxy3();
  const httpProxyLegacyServer = await setupHttpProxyLegacy();

  console.log("Validating proxy implementations...");

  const proxies = [
    { name: "httpxy server", port: HTTPXY_SERVER_PORT },
    { name: "httpxy proxyFetch", port: HTTPXY_FETCH_PORT },
    { name: "fast-proxy", port: FAST_PROXY_PORT },
    { name: "@fastify/http-proxy", port: FASTIFY_PROXY_PORT },
    { name: "http-proxy-3", port: HTTP_PROXY_3_PORT },
    { name: "http-proxy", port: HTTP_PROXY_PORT },
  ];

  let allValid = true;

  for (const { name, port } of proxies) {
    const errors: string[] = [];

    const getRes = await httpGet(port);
    if (getRes.status !== 200) {
      errors.push(`GET status=${getRes.status}, expected 200`);
    }
    if (getRes.body !== '{"ok":true}') {
      errors.push(`GET body=${JSON.stringify(getRes.body)}, expected '{"ok":true}'`);
    }

    const postSmall = await httpPost(port, SMALL_BODY);
    if (postSmall.status !== 200) {
      errors.push(`POST(1KB) status=${postSmall.status}, expected 200`);
    }
    if (postSmall.body !== SMALL_BODY) {
      errors.push(
        `POST(1KB) body mismatch: got ${postSmall.body.length} bytes, expected ${SMALL_BODY.length}`,
      );
    }

    const postLarge = await httpPost(port, LARGE_BODY);
    if (postLarge.status !== 200) {
      errors.push(`POST(100KB) status=${postLarge.status}, expected 200`);
    }
    if (postLarge.body !== LARGE_BODY) {
      errors.push(
        `POST(100KB) body mismatch: got ${postLarge.body.length} bytes, expected ${LARGE_BODY.length}`,
      );
    }

    if (errors.length > 0) {
      allValid = false;
      console.log(`  FAIL  ${name}`);
      for (const e of errors) {
        console.log(`        - ${e}`);
      }
    } else {
      console.log(`  OK    ${name}`);
    }
  }

  // Cleanup
  targetServer.close();
  httpxyServer.close();
  httpxyFetchServer.close();
  fastProxySetup.server.close();
  fastProxySetup.close();
  await fastifyApp.close();
  httpProxy3Server.close();
  httpProxyLegacyServer.close();

  if (!allValid) {
    console.error("\nValidation failed.");
    process.exit(1);
  }

  console.log("\nAll implementations valid.");
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});


================================================
FILE: build.config.mjs
================================================
import { defineBuildConfig } from "obuild/config";

export default defineBuildConfig({
  entries: [
    {
      type: "bundle",
      input: ["./src/index.ts"],
    },
  ],
});


================================================
FILE: package.json
================================================
{
  "name": "httpxy",
  "version": "0.5.1",
  "description": "A full-featured HTTP proxy for Node.js.",
  "license": "MIT",
  "repository": "unjs/httpxy",
  "files": [
    "dist"
  ],
  "type": "module",
  "sideEffects": false,
  "types": "./dist/index.d.mts",
  "exports": {
    ".": "./dist/index.mjs"
  },
  "scripts": {
    "build": "obuild",
    "dev": "vitest",
    "lint": "oxlint . && oxfmt --check",
    "fmt": "oxlint . --fix && oxfmt",
    "prepack": "pnpm run build",
    "release": "pnpm test && pnpm build && changelogen --release && npm publish && git push --follow-tags",
    "test": "pnpm lint && pnpm typecheck && vitest run --coverage",
    "typecheck": "tsgo --noEmit"
  },
  "devDependencies": {
    "@types/async": "^3.2.25",
    "@types/concat-stream": "^2.0.3",
    "@types/express": "^5.0.6",
    "@types/node": "^25.6.0",
    "@types/semver": "^7.7.1",
    "@types/sse": "^0.0.0",
    "@types/ws": "^8.18.1",
    "@typescript/native-preview": "^7.0.0-dev.20260421.1",
    "@vitest/coverage-v8": "^4.1.5",
    "async": "^3.2.6",
    "changelogen": "^0.6.2",
    "concat-stream": "^2.0.0",
    "eslint-config-unjs": "^0.6.2",
    "expect.js": "^0.3.1",
    "obuild": "^0.4.33",
    "ofetch": "^1.5.1",
    "oxfmt": "^0.46.0",
    "oxlint": "^1.61.0",
    "semver": "^7.7.4",
    "socket.io": "^4.8.3",
    "socket.io-client": "^4.8.3",
    "sse": "^0.0.8",
    "typescript": "^6.0.3",
    "undici": "^8.1.0",
    "vitest": "^4.1.5",
    "ws": "^8.20.0"
  },
  "packageManager": "pnpm@10.33.0"
}


================================================
FILE: playground/index.ts
================================================
import http from "node:http";
import { createProxyServer } from "../src/index.ts";

async function main() {
  const main = http.createServer((req, res) => {
    res.end(
      JSON.stringify({
        method: req.method,
        path: req.url,
        headers: req.headers,
      }),
    );
  });
  await new Promise<void>((resolve) => {
    main.listen(3000, "127.0.0.1", resolve);
  });

  const httpProxy = createProxyServer();

  const proxy = http.createServer(async (req, res) => {
    try {
      await httpProxy.web(req, res, {
        target: "http://127.0.0.1:3000",
      });
    } catch (error) {
      console.error(error);
      res.statusCode = 500;
      res.end("Proxy error: " + (error as Error).toString());
    }
  });
  await new Promise<void>((resolve) => {
    proxy.listen(3001, "127.0.0.1", resolve);
  });

  console.log("main: http://127.0.0.1:3000");
  console.log("proxy: http://127.0.0.1:3001");
}

// eslint-disable-next-line unicorn/prefer-top-level-await
main();


================================================
FILE: pnpm-workspace.yaml
================================================
packages:
  - bench


================================================
FILE: renovate.json
================================================
{
  "extends": ["github>unjs/renovate-config"]
}


================================================
FILE: src/_utils.ts
================================================
import httpNative from "node:http";
import httpsNative from "node:https";
import net from "node:net";
import type { ProxyAddr, ProxyServerOptions, ProxyTarget, ProxyTargetDetailed } from "./types.ts";
import type { Http2ServerRequest } from "node:http2";

const upgradeHeader = /(^|,)\s*upgrade\s*($|,)/i;

/**
 * Default keep-alive agents for connection reuse.
 */
export const defaultAgents = {
  http: new httpNative.Agent({ keepAlive: true, maxSockets: 256, maxFreeSockets: 64 }),
  https: new httpsNative.Agent({ keepAlive: true, maxSockets: 256, maxFreeSockets: 64 }),
};

/**
 * Simple Regex for testing if protocol is https
 */
export const isSSL = /^https|wss/;

/**
 * Node.js HTTP/2 accepts pseudo headers and it may conflict
 * with request options.
 *
 * Let's just blacklist those potential conflicting pseudo
 * headers.
 */
const HTTP2_HEADER_BLACKLIST = [":method", ":path", ":scheme", ":authority"];

/**
 * Copies the right headers from `options` and `req` to
 * `outgoing` which is then used to fire the proxied
 * request.
 *
 * Examples:
 *
 *    common.setupOutgoing(outgoing, options, req)
 *    // => { host: ..., hostname: ...}
 *
 * @param outgoing Base object to be filled with required properties
 * @param options Config object passed to the proxy
 * @param req Request Object
 * @param forward String to select forward or target
 *
 * @return Outgoing Object with all required properties set
 *
 * @api private
 */
export function setupOutgoing(
  outgoing: httpNative.RequestOptions & httpsNative.RequestOptions,
  options: ProxyServerOptions & {
    target: ProxyTarget;
    forward?: ProxyTarget;
    ca?: string;
    method?: string;
  },
  req: httpNative.IncomingMessage | Http2ServerRequest,
  forward?: "forward" | "target",
): httpNative.RequestOptions | httpsNative.RequestOptions {
  outgoing.port =
    (options[forward || "target"] as URL).port ||
    (isSSL.test((options[forward || "target"] as URL).protocol ?? "http") ? 443 : 80);

  for (const e of [
    "host",
    "hostname",
    "socketPath",
    "pfx",
    "key",
    "passphrase",
    "cert",
    "ca",
    "ciphers",
    "secureProtocol",
  ] as const) {
    const value = (options[forward || "target"] as ProxyTargetDetailed)[e];
    if (value !== undefined) {
      outgoing[e] = value as any;
    }
  }

  outgoing.method = options.method || req.method;
  outgoing.headers = { ...req.headers };

  // before clean up HTTP/2 blacklist header, we might wanna override host first
  if (req.headers?.[":authority"]) {
    outgoing.headers.host = req.headers[":authority"] as string;
  }
  // host override must happen before composing/merging the final outgoing headers

  if (options.headers) {
    for (const key of Object.keys(options.headers)) {
      outgoing.headers[key] = options.headers[key];
    }
  }

  if (req.httpVersionMajor > 1) {
    // ignore potential conflicting HTTP/2 pseudo headers
    for (const header of HTTP2_HEADER_BLACKLIST) {
      delete outgoing.headers[header];
    }
  }

  if (options.auth) {
    outgoing.auth = options.auth;
  }

  if (options.ca) {
    outgoing.ca = options.ca;
  }

  if (isSSL.test((options[forward || "target"] as URL).protocol ?? "http")) {
    outgoing.rejectUnauthorized = options.secure === undefined ? true : options.secure;
  }

  if (options.agent !== undefined) {
    outgoing.agent = options.agent || false;
  } else if (req.httpVersionMajor > 1 || upgradeHeader.test(req.headers.connection || "")) {
    // WebSocket upgrades and HTTP/2 incoming requests: agents conflict with
    // the socket lifecycle (upgrade handoff / stream multiplexing).
    outgoing.agent = false;
  } else {
    // Use default keep-alive agents for connection reuse
    const targetProto = (options[forward || "target"] as URL).protocol ?? "http";
    outgoing.agent = isSSL.test(targetProto) ? defaultAgents.https : defaultAgents.http;
  }
  outgoing.localAddress = options.localAddress;

  //
  // Remark: If we are false and not upgrading, set the connection: close. This is the right thing to do
  // as node core doesn't handle this COMPLETELY properly yet.
  //
  if (!outgoing.agent) {
    outgoing.headers = outgoing.headers || {};
    if (
      typeof outgoing.headers.connection !== "string" ||
      !upgradeHeader.test(outgoing.headers.connection)
    ) {
      outgoing.headers.connection = "close";
    }
  }

  // the final path is target path + relative path requested by user:
  const target = options[forward || "target"];
  const targetPath = target && options.prependPath !== false ? (target as URL).pathname || "" : "";
  const targetSearch =
    target instanceof URL && options.prependPath !== false ? target.search || "" : "";

  const reqUrl = req.url || "";
  const qIdx = reqUrl.indexOf("?");
  const reqPath = qIdx === -1 ? reqUrl : reqUrl.slice(0, qIdx);
  const reqSearch = qIdx === -1 ? "" : reqUrl.slice(qIdx);
  const normalizedPath = reqPath ? (reqPath[0] === "/" ? reqPath : "/" + reqPath) : "/";
  let outgoingPath = options.toProxy ? "/" + reqUrl : normalizedPath + reqSearch;

  //
  // Remark: ignorePath will just straight up ignore whatever the request's
  // path is. This can be labeled as FOOT-GUN material if you do not know what
  // you are doing and are using conflicting options.
  //
  outgoingPath = options.ignorePath ? "" : outgoingPath;

  let fullPath = joinURL(targetPath, outgoingPath);
  // Merge target query string into the outgoing path
  if (targetSearch) {
    const hasQuery = fullPath.includes("?");
    fullPath = hasQuery ? fullPath.replace("?", targetSearch + "&") : fullPath + targetSearch;
  }
  outgoing.path = fullPath;

  if (options.changeOrigin) {
    outgoing.headers.host =
      requiresPort(outgoing.port, (options[forward || "target"] as URL).protocol) &&
      !hasPort(outgoing.host)
        ? outgoing.host + ":" + outgoing.port
        : (outgoing.host ?? undefined);
  }
  return outgoing;
}

// From https://github.com/unjs/h3/blob/e8adfa/src/utils/internal/path.ts#L16C1-L36C2
export function joinURL(base: string | undefined, path: string | undefined): string {
  if (!base || base === "/") {
    return path || "/";
  }
  if (!path || path === "/") {
    return base || "/";
  }
  // eslint-disable-next-line unicorn/prefer-at
  const baseHasTrailing = base[base.length - 1] === "/";
  const pathHasLeading = path[0] === "/";
  if (baseHasTrailing && pathHasLeading) {
    return base + path.slice(1);
  }
  if (!baseHasTrailing && !pathHasLeading) {
    return base + "/" + path;
  }
  return base + path;
}

/**
 * Set the proper configuration for sockets,
 * set no delay and set keep alive, also set
 * the timeout to 0.
 *
 * Examples:
 *
 *    common.setupSocket(socket)
 *    // => Socket
 *
 * @param socket instance to setup
 *
 * @return Return the configured socket.
 *
 * @api private
 */

export function setupSocket(socket: net.Socket): net.Socket {
  socket.setTimeout(0);
  socket.setNoDelay(true);

  socket.setKeepAlive(true, 0);

  return socket;
}

/**
 * Get the port number from the host. Or guess it based on the connection type.
 *
 * @param req Incoming HTTP request.
 *
 * @return The port number.
 *
 * @api private
 */
export function getPort(req: httpNative.IncomingMessage | Http2ServerRequest): string {
  const hostHeader = (req.headers[":authority"] as string | undefined) || req.headers.host;
  const res = hostHeader ? hostHeader.match(/:(\d+)/) : "";
  if (res) {
    return res[1]!;
  }
  return hasEncryptedConnection(req) ? "443" : "80";
}

/**
 * Check if the request has an encrypted connection.
 *
 * @param req Incoming HTTP request.
 *
 * @return Whether the connection is encrypted or not.
 *
 * @api private
 */
export function hasEncryptedConnection(
  req: httpNative.IncomingMessage | Http2ServerRequest,
): boolean {
  const socket = req.socket;
  return !!socket && "encrypted" in socket && socket.encrypted;
}

/**
 * Rewrites or removes the domain of a cookie header
 *
 * @param header
 * @param config, mapping of domain to rewritten domain.
 *        '*' key to match any domain, null value to remove the domain.
 *
 * @api private
 */
export function rewriteCookieProperty(
  header: string,
  config: Record<string, string>,
  property: string,
): string;
export function rewriteCookieProperty(
  header: string | string[],
  config: Record<string, string>,
  property: string,
): string | string[];
export function rewriteCookieProperty(
  header: string | string[],
  config: Record<string, string>,
  property: string,
): string | string[] {
  if (Array.isArray(header)) {
    return header.map(function (headerElement) {
      return rewriteCookieProperty(headerElement, config, property);
    });
  }
  return header.replace(
    new RegExp(String.raw`(;\s*` + property + "=)([^;]+)", "i"),
    function (match, prefix, previousValue) {
      let newValue;
      if (previousValue in config) {
        newValue = config[previousValue];
      } else if ("*" in config) {
        newValue = config["*"];
      } else {
        // no match, return previous value
        return match;
      }
      // replace or remove value
      return newValue ? prefix + newValue : "";
    },
  );
}

/**
 * Parse and validate a proxy address.
 *
 * @param addr - URL string (`http://host:port`, `ws://host:port`, `unix:/path`) or a `ProxyAddr` object.
 *
 * @api private
 */
export function parseAddr(addr: string | ProxyAddr): ProxyAddr {
  if (typeof addr === "string") {
    if (addr.startsWith("unix:")) {
      return { socketPath: addr.slice(5) };
    }
    const url = new URL(addr);
    return {
      host: url.hostname,
      port: Number(url.port) || (isSSL.test(url.protocol) ? 443 : 80),
    };
  }
  if (!addr.socketPath && !addr.port) {
    throw new Error("ProxyAddr must have either `port` or `socketPath`");
  }
  return addr;
}

/**
 * Check the host and see if it potentially has a port in it (keep it simple)
 *
 * @returns Whether we have one or not
 *
 * @api private
 */
export function hasPort(host: string | null | undefined): boolean {
  return host ? !!~host.indexOf(":") : false;
}

/**
 * Check if the port is required for the protocol
 *
 * Ported from https://github.com/unshiftio/requires-port/blob/master/index.js
 *
 * @returns Whether the port is required for the protocol
 *
 * @api private
 */
export function requiresPort(_port: string | number, _protocol: string | undefined): boolean {
  const protocol = _protocol?.split(":")[0];
  const port = +_port;

  if (!port) return false;

  switch (protocol) {
    case "http":
    case "ws": {
      return port !== 80;
    }

    case "https":
    case "wss": {
      return port !== 443;
    }

    case "ftp": {
      return port !== 21;
    }

    case "gopher": {
      return port !== 70;
    }

    case "file": {
      return false;
    }
  }

  return port !== 0;
}


================================================
FILE: src/fetch.ts
================================================
import type { IncomingMessage, RequestOptions } from "node:http";
import { request as httpRequest } from "node:http";
import { request as httpsRequest } from "node:https";
import { Readable } from "node:stream";
import type { ProxyAddr } from "./types.ts";
import { defaultAgents, isSSL, joinURL, parseAddr } from "./_utils.ts";

/**
 * Options for {@link proxyFetch}.
 */
export interface ProxyFetchOptions {
  /**
   * Timeout in milliseconds for the upstream request.
   * Rejects with an error if the upstream does not respond within this time.
   */
  timeout?: number;
  /**
   * Add `x-forwarded-for`, `x-forwarded-port`, `x-forwarded-proto`, and
   * `x-forwarded-host` headers derived from the input URL.
   * Default: `false`.
   */
  xfwd?: boolean;
  /**
   * Rewrite the `Host` header to match the target address.
   * Default: `false` (original host from the input URL is kept).
   */
  changeOrigin?: boolean;
  /**
   * HTTP agent for connection pooling / reuse.
   * Default: `false` (no agent, no keep-alive).
   */
  agent?: any;
  /**
   * Follow HTTP redirects from the upstream.
   * `true` = max 5 hops; number = custom max.
   * Default: `false` (manual redirect, raw 3xx responses are returned).
   */
  followRedirects?: boolean | number;
  /**
   * TLS options forwarded to `https.request` (e.g. `{ rejectUnauthorized: false }`).
   * Also controls certificate verification — set `rejectUnauthorized: false` to skip.
   * Default: none.
   */
  ssl?: Record<string, unknown>;
}

/**
 * Proxy a request to a specific server address (TCP host/port or Unix socket)
 * using web standard {@link Request}/{@link Response} interfaces.
 *
 * Supports both HTTP and HTTPS upstream targets.
 *
 * @param addr - The target server address. Can be a URL string (`http://host:port`, `https://host:port`, `unix:/path`), or an object with `host`/`port` for TCP or `socketPath` for Unix sockets.
 * @param input - The request URL (string or URL) or a {@link Request} object.
 * @param inputInit - Optional {@link RequestInit} or {@link Request} to override method, headers, and body.
 * @param opts - Optional proxy options.
 */
export async function proxyFetch(
  addr: string | ProxyAddr,
  input: string | URL | Request,
  inputInit?: RequestInit | Request,
  opts?: ProxyFetchOptions,
) {
  const resolvedAddr = parseAddr(addr);

  // Detect protocol and base path from addr string
  let useHTTPS = false;
  let addrBasePath = "";
  if (typeof addr === "string" && !addr.startsWith("unix:")) {
    const addrURL = new URL(addr);
    useHTTPS = isSSL.test(addrURL.protocol);
    if (addrURL.pathname && addrURL.pathname !== "/") {
      addrBasePath = addrURL.pathname;
    }
  }

  let url: URL;
  let init: RequestInit | undefined;

  if (input instanceof Request) {
    url = new URL(input.url);
    init = {
      ...toInit(input),
      ...toInit(inputInit),
    };
  } else {
    url = new URL(input);
    init = toInit(inputInit);
  }
  init = {
    redirect: "manual",
    ...init,
  };
  if (init.body) {
    (init as RequestInit & { duplex: string }).duplex = "half";
  }

  // Merge addr base path with request path
  const requestPath = url.pathname + url.search;
  const path = addrBasePath ? joinURL(addrBasePath, requestPath) : requestPath;

  const reqHeaders: Record<string, string | string[]> = {};
  if (init.headers) {
    // Fast path: plain object — direct assign, no iteration needed
    if (!(init.headers instanceof Headers) && !Array.isArray(init.headers)) {
      Object.assign(reqHeaders, init.headers);
    } else {
      // Headers or [key, value][] — both are iterable pairs
      for (const [key, value] of init.headers as Iterable<[string, string]>) {
        const existing = reqHeaders[key];
        if (existing === undefined) {
          reqHeaders[key] = value;
        } else {
          reqHeaders[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
        }
      }
    }
  }

  // Add x-forwarded-* headers derived from the input URL
  if (opts?.xfwd) {
    if (!reqHeaders["x-forwarded-for"]) {
      reqHeaders["x-forwarded-for"] = url.hostname;
    }
    if (!reqHeaders["x-forwarded-port"]) {
      reqHeaders["x-forwarded-port"] = url.port || (url.protocol === "https:" ? "443" : "80");
    }
    if (!reqHeaders["x-forwarded-proto"]) {
      reqHeaders["x-forwarded-proto"] = url.protocol.replace(":", "");
    }
    if (!reqHeaders["x-forwarded-host"]) {
      reqHeaders["x-forwarded-host"] = url.host;
    }
  }

  // Rewrite Host header to match the target address
  if (opts?.changeOrigin) {
    if (resolvedAddr.socketPath) {
      reqHeaders.host = "localhost";
    } else {
      const targetHost = resolvedAddr.host || "localhost";
      const targetPort = resolvedAddr.port;
      const defaultPort = useHTTPS ? 443 : 80;
      reqHeaders.host =
        targetPort && targetPort !== defaultPort ? `${targetHost}:${targetPort}` : targetHost;
    }
  }

  const maxRedirects =
    typeof opts?.followRedirects === "number"
      ? opts.followRedirects
      : opts?.followRedirects
        ? 5
        : 0;

  // Buffer body only when redirects need replay; otherwise stream through
  const body = maxRedirects > 0 ? await _bufferBody(init.body) : _toNodeStream(init.body);

  // Default to keep-alive agent for connection reuse
  const agent =
    opts?.agent !== undefined
      ? opts.agent || false
      : useHTTPS
        ? defaultAgents.https
        : defaultAgents.http;

  const res = await _sendRequest(
    useHTTPS ? httpsRequest : httpRequest,
    init.method || "GET",
    path,
    reqHeaders,
    resolvedAddr,
    body,
    {
      signal: init.signal || undefined,
      agent,
      timeout: opts?.timeout,
      ssl: opts?.ssl,
      maxRedirects,
      redirectCount: 0,
      originalHeaders: reqHeaders,
    },
  );

  // Build Response — use plain header pairs to avoid Headers object overhead
  const resHeaders: [string, string][] = [];
  const rawHeaders = res.rawHeaders;
  for (let i = 0; i < rawHeaders.length; i += 2) {
    const key = rawHeaders[i]!;
    const keyLower = key.toLowerCase();
    if (
      keyLower === "transfer-encoding" ||
      keyLower === "keep-alive" ||
      keyLower === "connection"
    ) {
      continue;
    }
    resHeaders.push([key, rawHeaders[i + 1]!]);
  }

  const hasBody = res.statusCode !== 204 && res.statusCode !== 304;
  return new Response(hasBody ? (Readable.toWeb(res) as ReadableStream) : null, {
    status: res.statusCode,
    statusText: res.statusMessage,
    headers: resHeaders,
  });
}

// --- Internal ---

function toInit(init?: RequestInit | Request): RequestInit | undefined {
  if (!init) {
    return undefined;
  }
  if (init instanceof Request) {
    return {
      method: init.method,
      headers: init.headers,
      body: init.body,
      duplex: init.body ? "half" : undefined,
    } as RequestInit;
  }
  return init;
}

/** Convert body to a Node.js Readable or Buffer for streaming without buffering. */
function _toNodeStream(body: BodyInit | null | undefined): Readable | Buffer | undefined {
  if (!body) {
    return undefined;
  }
  if (typeof body === "string") {
    return Buffer.from(body);
  }
  if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
    return Buffer.from(body as ArrayBuffer);
  }
  if (body instanceof ReadableStream) {
    return Readable.fromWeb(body as import("node:stream/web").ReadableStream);
  }
  if (body instanceof Blob) {
    return Readable.fromWeb(body.stream() as import("node:stream/web").ReadableStream);
  }
  return Buffer.from(String(body));
}

/** Normalize any body type to Buffer (or undefined) for redirect replay. */
async function _bufferBody(body: BodyInit | null | undefined): Promise<Buffer | undefined> {
  if (!body) {
    return undefined;
  }
  if (typeof body === "string") {
    return Buffer.from(body);
  }
  if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
    return Buffer.from(body as ArrayBuffer);
  }
  if (body instanceof ReadableStream) {
    const readable = Readable.fromWeb(body as import("node:stream/web").ReadableStream);
    const chunks: Buffer[] = [];
    for await (const chunk of readable) {
      chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
    }
    return Buffer.concat(chunks);
  }
  if (body instanceof Blob) {
    return Buffer.from(await body.arrayBuffer());
  }
  return Buffer.from(String(body));
}

const _redirectStatuses = new Set([301, 302, 303, 307, 308]);

interface _RequestOpts {
  signal?: AbortSignal;
  agent?: any;
  timeout?: number;
  ssl?: Record<string, unknown>;
  maxRedirects: number;
  redirectCount: number;
  originalHeaders: Record<string, string | string[]>;
}

function _sendRequest(
  doRequest: typeof httpRequest,
  method: string,
  path: string,
  headers: Record<string, string | string[]>,
  addr: ProxyAddr,
  body: Buffer | Readable | undefined,
  opts: _RequestOpts,
): Promise<IncomingMessage> {
  return new Promise<IncomingMessage>((resolve, reject) => {
    const reqOpts: RequestOptions = {
      method,
      path,
      headers,
      agent: opts.agent,
    };

    if (addr.socketPath) {
      reqOpts.socketPath = addr.socketPath;
    } else {
      reqOpts.hostname = addr.host || "localhost";
      reqOpts.port = addr.port;
    }

    if (opts.signal) {
      reqOpts.signal = opts.signal;
    }

    if (opts.ssl) {
      Object.assign(reqOpts, opts.ssl);
    }

    const req = doRequest(reqOpts, (res) => {
      const statusCode = res.statusCode!;

      if (
        opts.maxRedirects > 0 &&
        _redirectStatuses.has(statusCode) &&
        opts.redirectCount < opts.maxRedirects &&
        res.headers.location
      ) {
        res.resume();

        const currentURL = new URL(path, `http://${addr.host || "localhost"}:${addr.port || 80}`);
        const location = new URL(res.headers.location, currentURL);
        const redirectHTTPS = isSSL.test(location.protocol);

        const preserveMethod = statusCode === 307 || statusCode === 308;
        const redirectMethod = preserveMethod ? method : "GET";

        const redirectHeaders: Record<string, string | string[]> = {
          ...opts.originalHeaders,
        };
        redirectHeaders.host = location.host;

        if (location.host !== currentURL.host) {
          delete redirectHeaders.authorization;
          delete redirectHeaders.cookie;
        }

        if (!preserveMethod) {
          delete redirectHeaders["content-length"];
          delete redirectHeaders["content-type"];
          delete redirectHeaders["transfer-encoding"];
        }

        _sendRequest(
          redirectHTTPS ? httpsRequest : httpRequest,
          redirectMethod,
          location.pathname + location.search,
          redirectHeaders,
          {
            host: location.hostname,
            port: Number(location.port) || (redirectHTTPS ? 443 : 80),
          },
          preserveMethod ? body : undefined,
          { ...opts, redirectCount: opts.redirectCount + 1 },
        ).then(resolve, reject);
        return;
      }

      resolve(res);
    });

    req.on("error", reject);

    if (opts.timeout) {
      req.setTimeout(opts.timeout, () => {
        req.destroy(new Error("Proxy request timed out"));
      });
    }

    if (body instanceof Readable) {
      body.on("error", (err) => {
        req.destroy(err);
        reject(err);
      });
      body.pipe(req);
    } else if (body) {
      req.end(body);
    } else {
      req.end();
    }
  });
}


================================================
FILE: src/index.ts
================================================
export type { ProxyAddr, ProxyServerOptions, ProxyTarget, ProxyTargetDetailed } from "./types.ts";
export { ProxyServer, createProxyServer, type ProxyServerEventMap } from "./server.ts";
export { proxyFetch, type ProxyFetchOptions } from "./fetch.ts";
export { proxyUpgrade, type ProxyUpgradeOptions } from "./ws.ts";


================================================
FILE: src/middleware/_utils.ts
================================================
import type { IncomingMessage, ServerResponse } from "node:http";
import type { Socket } from "node:net";
import type { ProxyServer } from "../server.ts";
import type { ProxyServerOptions, ProxyTargetDetailed } from "../types.ts";
import type { Http2ServerRequest, Http2ServerResponse } from "node:http2";

export type ResOfType<T extends "web" | "ws"> = T extends "ws"
  ? T extends "web"
    ? ServerResponse | Http2ServerResponse | Socket
    : Socket
  : T extends "web"
    ? ServerResponse | Http2ServerResponse
    : never;

export type ProxyMiddleware<T extends ServerResponse | Http2ServerResponse | Socket> = (
  req: IncomingMessage | Http2ServerRequest,
  res: T,
  opts: ProxyServerOptions & {
    target: URL | ProxyTargetDetailed;
    forward: URL;
  },
  server: ProxyServer<IncomingMessage | Http2ServerRequest, ServerResponse | Http2ServerResponse>,
  head?: Buffer,
  callback?: (err: any, req: IncomingMessage | Http2ServerRequest, socket: T, url?: any) => void,
) => void | true;

export function defineProxyMiddleware<T extends ServerResponse | Socket = ServerResponse>(
  m: ProxyMiddleware<T>,
) {
  return m;
}

export type ProxyOutgoingMiddleware = (
  req: IncomingMessage | Http2ServerRequest,
  res: ServerResponse | Http2ServerResponse,
  proxyRes: IncomingMessage,
  opts: ProxyServerOptions & {
    target: URL | ProxyTargetDetailed;
    forward: URL;
  },
) => void | true;

export function defineProxyOutgoingMiddleware(m: ProxyOutgoingMiddleware) {
  return m;
}


================================================
FILE: src/middleware/web-incoming.ts
================================================
import type { ClientRequest, IncomingMessage, ServerResponse } from "node:http";
import type { ProxyTargetDetailed } from "../types.ts";
import nodeHTTP from "node:http";
import nodeHTTPS from "node:https";
import { getPort, hasEncryptedConnection, isSSL, setupOutgoing } from "../_utils.ts";
import { webOutgoingMiddleware } from "./web-outgoing.ts";
import { type ProxyMiddleware, defineProxyMiddleware } from "./_utils.ts";

const nativeAgents = { http: nodeHTTP, https: nodeHTTPS };
const redirectStatuses = new Set([301, 302, 303, 307, 308]);

/**
 * Sets `content-length` to '0' if request is of DELETE type.
 */
export const deleteLength = defineProxyMiddleware((req) => {
  if ((req.method === "DELETE" || req.method === "OPTIONS") && !req.headers["content-length"]) {
    req.headers["content-length"] = "0";
    delete req.headers["transfer-encoding"];
  }
});

/**
 * Sets timeout in request socket if it was specified in options.
 */
export const timeout = defineProxyMiddleware((req, res, options) => {
  if (options.timeout) {
    req.socket.setTimeout(options.timeout, () => {
      req.socket.destroy();
    });
  }
});

/**
 * Sets `x-forwarded-*` headers if specified in config.
 */
export const XHeaders = defineProxyMiddleware((req, res, options) => {
  if (!options.xfwd) {
    return;
  }

  const encrypted = (req as any).isSpdy || hasEncryptedConnection(req);
  const values = {
    for: req.connection.remoteAddress || req.socket.remoteAddress,
    port: getPort(req),
    proto: encrypted ? "https" : "http",
  };

  for (const header of ["for", "port", "proto"] as const) {
    const key = "x-forwarded-" + header;
    if (!req.headers[key] && values[header] !== undefined) {
      req.headers[key] = values[header];
    }
  }

  req.headers["x-forwarded-host"] =
    req.headers["x-forwarded-host"] || req.headers[":authority"] || req.headers.host || "";
});

/**
 * Does the actual proxying. If `forward` is enabled fires up
 * a ForwardStream, same happens for ProxyStream. The request
 * just dies otherwise.
 *
 */
export const stream = defineProxyMiddleware((req, res, options, server, head, callback) => {
  // And we begin!
  server.emit("start", req, res, options.target || options.forward);

  const http = nativeAgents.http;
  const https = nativeAgents.https;

  const maxRedirects =
    typeof options.followRedirects === "number"
      ? options.followRedirects
      : options.followRedirects
        ? 5
        : 0;

  if (options.forward) {
    // If forward enable, so just pipe the request
    const forwardReq = (isSSL.test(options.forward.protocol || "http") ? https : http).request(
      setupOutgoing(options.ssl || {}, options, req, "forward"),
    );

    // error handler (e.g. ECONNRESET, ECONNREFUSED)
    // Handle errors on incoming request as well as it makes sense to
    const forwardError = createErrorHandler(forwardReq, options.forward);
    req.on("error", forwardError);
    forwardReq.on("error", forwardError);

    (options.buffer || req).pipe(forwardReq);
    if (!options.target) {
      res.end();
      return;
    }
  }

  // Request initalization
  const proxyReq = (isSSL.test(options.target.protocol || "http") ? https : http).request(
    setupOutgoing(options.ssl || {}, options, req),
  );

  // Enable developers to modify the proxyReq before headers are sent
  proxyReq.on("socket", (_socket) => {
    if (server && !proxyReq.getHeader("expect")) {
      server.emit("proxyReq", proxyReq, req, res, options);
    }
  });

  // allow outgoing socket to timeout so that we could
  // show an error page at the initial request
  if (options.proxyTimeout) {
    proxyReq.setTimeout(options.proxyTimeout, function () {
      proxyReq.destroy();
    });
  }

  // Abort proxy request when client disconnects
  res.on("close", function () {
    if (!res.writableFinished) {
      proxyReq.destroy();
    }
  });

  // handle errors in proxy and incoming request, just like for forward proxy
  const proxyError = createErrorHandler(proxyReq, options.target);
  req.on("error", proxyError);
  proxyReq.on("error", proxyError);

  function createErrorHandler(proxyReq: ClientRequest, url: URL | ProxyTargetDetailed) {
    return function proxyError(err: Error) {
      if (!req.socket?.writable && (err as NodeJS.ErrnoException).code === "ECONNRESET") {
        server.emit("econnreset", err, req, res, url);
        return proxyReq.destroy();
      }

      if (callback) {
        callback(err, req, res, url);
      } else {
        server.emit("error", err, req, res, url);
      }
    };
  }

  // Buffer request body when following redirects (needed for 307/308 replay)
  let bodyBuffer: Buffer | undefined;
  if (maxRedirects > 0) {
    const chunks: Buffer[] = [];
    const source = options.buffer || req;
    source.on("data", (chunk: Buffer) => {
      chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
      proxyReq.write(chunk);
    });
    source.on("end", () => {
      bodyBuffer = Buffer.concat(chunks);
      proxyReq.end();
    });
    source.on("error", (err: Error) => {
      proxyReq.destroy(err);
    });
  } else {
    proxyReq.on("socket", (socket) => {
      if (socket.pending) {
        socket.on("connect", () => (options.buffer || req).pipe(proxyReq));
      } else {
        (options.buffer || req).pipe(proxyReq);
      }
    });
  }

  function handleResponse(proxyRes: IncomingMessage, redirectCount: number, currentUrl: URL) {
    const statusCode = proxyRes.statusCode!;

    if (
      maxRedirects > 0 &&
      redirectStatuses.has(statusCode) &&
      redirectCount < maxRedirects &&
      proxyRes.headers.location
    ) {
      // Drain the redirect response body
      proxyRes.resume();

      const location = new URL(proxyRes.headers.location, currentUrl);

      // 301/302/303 → GET without body; 307/308 → preserve method and body
      const preserveMethod = statusCode === 307 || statusCode === 308;
      const redirectMethod = preserveMethod ? req.method || "GET" : "GET";

      const isHTTPS = isSSL.test(location.protocol);
      const agent = isHTTPS ? https : http;

      // Build headers from original request
      const redirectHeaders: Record<string, string | string[] | undefined> = { ...req.headers };
      if (options.headers) {
        Object.assign(redirectHeaders, options.headers);
      }
      redirectHeaders.host = location.host;

      // Strip sensitive headers on cross-origin redirects
      if (location.host !== currentUrl.host) {
        delete redirectHeaders.authorization;
        delete redirectHeaders.cookie;
      }

      // Drop body-related headers when method changes to GET
      if (!preserveMethod) {
        delete redirectHeaders["content-length"];
        delete redirectHeaders["content-type"];
        delete redirectHeaders["transfer-encoding"];
      }

      const redirectOpts: nodeHTTP.RequestOptions = {
        hostname: location.hostname,
        port: location.port || (isHTTPS ? 443 : 80),
        path: location.pathname + location.search,
        method: redirectMethod,
        headers: redirectHeaders,
        agent: options.agent || false,
      };

      if (isHTTPS) {
        (redirectOpts as nodeHTTPS.RequestOptions).rejectUnauthorized =
          options.secure === undefined ? true : options.secure;
      }

      const redirectReq = agent.request(redirectOpts);

      if (server && !redirectReq.getHeader("expect")) {
        server.emit("proxyReq", redirectReq, req, res, options);
      }

      if (options.proxyTimeout) {
        redirectReq.setTimeout(options.proxyTimeout, () => {
          redirectReq.destroy();
        });
      }

      const redirectError = createErrorHandler(redirectReq, location);
      redirectReq.on("error", redirectError);

      redirectReq.on("response", (nextRes: IncomingMessage) => {
        handleResponse(nextRes, redirectCount + 1, location);
      });

      if (preserveMethod && bodyBuffer && bodyBuffer.length > 0) {
        redirectReq.end(bodyBuffer);
      } else {
        redirectReq.end();
      }

      return;
    }

    // Non-redirect response (or max redirects exceeded)
    if (server) {
      server.emit("proxyRes", proxyRes, req, res);
    }

    if (!res.headersSent && !options.selfHandleResponse) {
      for (const pass of webOutgoingMiddleware) {
        if (pass(req, res, proxyRes, options)) {
          break;
        }
      }
    }

    if (res.finished) {
      if (server) {
        server.emit("end", req, res, proxyRes);
      }
    } else {
      res.on("close", function () {
        proxyRes.destroy();
      });
      proxyRes.on("close", function () {
        if (!proxyRes.complete && !res.destroyed) {
          res.destroy();
        }
      });
      proxyRes.on("error", function (err) {
        if (!res.destroyed) {
          res.destroy(err);
        }

        if (server.listenerCount("error") > 0) {
          server.emit("error", err, req, res, currentUrl);
        }
      });
      proxyRes.on("end", function () {
        if (server) {
          server.emit("end", req, res, proxyRes);
        }
      });
      if (!options.selfHandleResponse) {
        proxyRes.pipe(res);
      }
    }
  }

  proxyReq.on("response", function (proxyRes) {
    handleResponse(proxyRes, 0, options.target as URL);
  });
});

export const webIncomingMiddleware: readonly ProxyMiddleware<ServerResponse>[] = [
  deleteLength,
  timeout,
  XHeaders,
  stream,
] as const;


================================================
FILE: src/middleware/web-outgoing.ts
================================================
import { rewriteCookieProperty } from "../_utils.ts";
import type { ProxyTarget, ProxyTargetDetailed } from "../types.ts";
import { type ProxyOutgoingMiddleware, defineProxyOutgoingMiddleware } from "./_utils.ts";

const redirectRegex = /^201|30([12378])$/;

/**
 * Remove chunked transfer-encoding for HTTP/1.0, HTTP/2, and bodyless (204/304) responses
 */
export const removeChunked = defineProxyOutgoingMiddleware((req, res, proxyRes) => {
  // HTTP/1.0 and HTTP/2 do not have transfer-encoding: chunked
  // 204 and 304 responses MUST NOT contain a message body (RFC 9110)
  if (
    req.httpVersion === "1.0" ||
    req.httpVersionMajor >= 2 ||
    proxyRes.statusCode === 204 ||
    proxyRes.statusCode === 304
  ) {
    delete proxyRes.headers["transfer-encoding"];
  }
});

/**
 * If is a HTTP 1.0 request, set the correct connection header
 * or if connection header not present, then use `keep-alive`
 *
 * If is a HTTP/2 request, remove connection header no matter what,
 * this avoids sending connection header to the underlying http2 client
 */
export const setConnection = defineProxyOutgoingMiddleware((req, res, proxyRes) => {
  if (req.httpVersion === "1.0") {
    proxyRes.headers.connection = req.headers.connection || "close";
  } else if (req.httpVersionMajor < 2 && !proxyRes.headers.connection) {
    proxyRes.headers.connection = req.headers.connection || "keep-alive";
  } else if (req.httpVersionMajor >= 2) {
    delete proxyRes.headers.connection;
  }
});

export const setRedirectHostRewrite = defineProxyOutgoingMiddleware(
  (req, res, proxyRes, options) => {
    if (
      (options.hostRewrite || options.autoRewrite || options.protocolRewrite) &&
      proxyRes.headers.location &&
      redirectRegex.test(String(proxyRes.statusCode))
    ) {
      const target = _toURL(options.target!);
      const u = new URL(proxyRes.headers.location, target);

      // Make sure the redirected host matches the target host before rewriting
      if (target.host !== u.host) {
        return;
      }

      if (options.hostRewrite) {
        u.host = options.hostRewrite;
      } else if (options.autoRewrite) {
        if (req.headers[":authority"]) {
          u.host = req.headers[":authority"] as string;
        } else if (req.headers.host) {
          u.host = req.headers.host;
        }
      }
      if (options.protocolRewrite) {
        u.protocol = options.protocolRewrite;
      }

      proxyRes.headers.location = u.toString();
    }
  },
);

/**
 * Copy headers from proxyResponse to response
 * set each header in response object.
 *
 * @param {ClientRequest} Req Request object
 * @param {IncomingMessage} Res Response object
 * @param {proxyResponse} Res Response object from the proxy request
 * @param {Object} Options options.cookieDomainRewrite: Config to rewrite cookie domain
 *
 * @api private
 */
export const writeHeaders = defineProxyOutgoingMiddleware((req, res, proxyRes, options) => {
  const rewriteCookieDomainConfig =
    typeof options.cookieDomainRewrite === "string"
      ? // also test for ''
        { "*": options.cookieDomainRewrite }
      : options.cookieDomainRewrite;
  const rewriteCookiePathConfig =
    typeof options.cookiePathRewrite === "string"
      ? // also test for ''
        { "*": options.cookiePathRewrite }
      : options.cookiePathRewrite;

  const preserveHeaderKeyCase = options.preserveHeaderKeyCase;
  let rawHeaderKeyMap: Record<string, string> | undefined;
  const setHeader = function (key: string, header: string | string[] | undefined) {
    if (header === undefined || !String(key).trim()) {
      return;
    }
    if (rewriteCookieDomainConfig && key.toLowerCase() === "set-cookie") {
      header = rewriteCookieProperty(header, rewriteCookieDomainConfig, "domain");
    }
    if (rewriteCookiePathConfig && key.toLowerCase() === "set-cookie") {
      header = rewriteCookieProperty(header, rewriteCookiePathConfig, "path");
    }
    try {
      res.setHeader(String(key).trim(), header);
    } catch {
      // Skip headers with invalid characters (e.g. control chars)
      // to avoid crashing with ERR_INVALID_CHAR
    }
  };

  // message.rawHeaders is added in: v0.11.6
  // https://nodejs.org/api/http.html#http_message_rawheaders
  if (preserveHeaderKeyCase && proxyRes.rawHeaders !== undefined) {
    rawHeaderKeyMap = {};
    for (let i = 0; i < proxyRes.rawHeaders.length; i += 2) {
      const key = proxyRes.rawHeaders[i]!;
      rawHeaderKeyMap[key.toLowerCase()] = key;
    }
  }

  for (let key of Object.keys(proxyRes.headers)) {
    const header = proxyRes.headers[key];
    if (preserveHeaderKeyCase && rawHeaderKeyMap) {
      key = rawHeaderKeyMap[key] || key;
    }
    setHeader(key, header);
  }
});

/**
 * Set the statusCode from the proxyResponse
 */
export const writeStatusCode = defineProxyOutgoingMiddleware((req, res, proxyRes) => {
  // From Node.js docs: response.writeHead(statusCode[, statusMessage][, headers])

  // @ts-expect-error
  res.statusCode = proxyRes.statusCode;

  if (
    proxyRes.statusMessage &&
    // Only HTTP/1.0 and HTTP/1.1 support statusMessage
    req.httpVersionMajor < 2
  ) {
    res.statusMessage = proxyRes.statusMessage;
  }
});

export const webOutgoingMiddleware: readonly ProxyOutgoingMiddleware[] = [
  removeChunked,
  setConnection,
  setRedirectHostRewrite,
  writeHeaders,
  writeStatusCode,
] as const;

// --- Internal ---

function _toURL(target: ProxyTarget): URL {
  if (target instanceof URL) {
    return target;
  }
  if (typeof target === "string") {
    return new URL(target);
  }
  const protocol = (target as ProxyTargetDetailed).protocol || "http:";
  const host =
    (target as ProxyTargetDetailed).host || (target as ProxyTargetDetailed).hostname || "localhost";
  const port = (target as ProxyTargetDetailed).port;
  return new URL(`${protocol}//${host}${port ? ":" + port : ""}`);
}


================================================
FILE: src/middleware/ws-incoming.ts
================================================
import nodeHTTP from "node:http";
import nodeHTTPS from "node:https";
import type { Socket } from "node:net";
import { type ProxyMiddleware, defineProxyMiddleware } from "./_utils.ts";
import { getPort, hasEncryptedConnection, isSSL, setupOutgoing, setupSocket } from "../_utils.ts";

/**
 * WebSocket requests must have the `GET` method and
 * the `upgrade:websocket` header
 */
export const checkMethodAndHeader = defineProxyMiddleware<Socket>((req, socket) => {
  if (req.method !== "GET" || !req.headers.upgrade) {
    socket.destroy();
    return true;
  }

  if (req.headers.upgrade.toLowerCase() !== "websocket") {
    socket.destroy();
    return true;
  }
});

/**
 * Sets `x-forwarded-*` headers if specified in config.
 */
export const XHeaders = defineProxyMiddleware<Socket>((req, socket, options) => {
  if (!options.xfwd) {
    return;
  }

  const values = {
    for: req.connection.remoteAddress || req.socket.remoteAddress,
    port: getPort(req),
    proto: hasEncryptedConnection(req) ? "wss" : "ws",
  };

  for (const header of ["for", "port", "proto"] as const) {
    const key = "x-forwarded-" + header;
    if (!req.headers[key] && values[header] !== undefined) {
      req.headers[key] = values[header];
    }
  }
});

/**
 * Does the actual proxying. Make the request and upgrade it
 * send the Switching Protocols request and pipe the sockets.
 */
export const stream = defineProxyMiddleware<Socket>(
  (req, socket, options, server, head, callback) => {
    const createHttpHeader = function (line: string, headers: nodeHTTP.OutgoingHttpHeaders) {
      return (
        Object.keys(headers)
          // eslint-disable-next-line unicorn/no-array-reduce
          .reduce(
            function (head, key) {
              const value = headers[key];

              if (!Array.isArray(value)) {
                head.push(key + ": " + value);
                return head;
              }

              for (const element of value) {
                head.push(key + ": " + element);
              }
              return head;
            },
            [line],
          )
          .join("\r\n") + "\r\n\r\n"
      );
    };

    setupSocket(socket);

    if (head && head.length > 0) {
      socket.unshift(head);
    }

    // Attach error handler early so client socket errors (e.g. ECONNRESET
    // from an intermediary proxy timeout) are caught before the upstream
    // upgrade response arrives. (#79)
    socket.on("error", onSocketError);

    const proxyReq = (isSSL.test(options.target.protocol || "http") ? nodeHTTPS : nodeHTTP).request(
      setupOutgoing(options.ssl || {}, options, req),
    );

    // Enable developers to modify the proxyReq before headers are sent
    if (server) {
      server.emit("proxyReqWs", proxyReq, req, socket, options, head);
    }

    // Error Handler
    proxyReq.on("error", onOutgoingError);
    proxyReq.on("response", function (res) {
      // if upgrade event isn't going to happen, close the socket
      // guard against writing to an already-destroyed socket
      // (https://github.com/http-party/node-http-proxy/pull/1433)
      if (!(res as any).upgrade) {
        if (!socket.destroyed && socket.writable) {
          socket.write(
            createHttpHeader(
              "HTTP/" + res.httpVersion + " " + res.statusCode + " " + res.statusMessage,
              res.headers,
            ),
          );
          res.on("error", onOutgoingError);
          res.pipe(socket);
        } else {
          // Socket already gone — consume response to avoid unhandled stream errors
          res.resume();
        }
      }
    });

    proxyReq.on("upgrade", function (proxyRes, proxySocket, proxyHead) {
      proxySocket.on("error", onOutgoingError);

      // Allow us to listen when the websocket has completed
      proxySocket.on("end", function () {
        server.emit("close", proxyRes, proxySocket, proxyHead);
      });

      // Remove the pre-upgrade error handler — it calls proxyReq.destroy()
      // which is pointless after upgrade and emits a spurious error event.
      socket.removeListener("error", onSocketError);

      // The pipe below will end proxySocket if socket closes cleanly, but not
      // if it errors (eg, vanishes from the net and starts returning
      // EHOSTUNREACH). We need to do that explicitly.
      socket.on("error", function () {
        proxySocket.end();
      });

      setupSocket(proxySocket);

      if (proxyHead && proxyHead.length > 0) {
        proxySocket.unshift(proxyHead);
      }

      //
      // Remark: Handle writing the headers to the socket when switching protocols
      // Also handles when a header is an array
      //
      socket.write(createHttpHeader("HTTP/1.1 101 Switching Protocols", proxyRes.headers));

      proxySocket.pipe(socket).pipe(proxySocket);

      server.emit("open", proxySocket);
      server.emit("proxySocket", proxySocket); // DEPRECATED.
    });

    proxyReq.end(); // XXX: CHECK IF THIS IS THIS CORRECT
    // return;

    function onSocketError(err: Error) {
      if (callback) {
        callback(err, req, socket);
      } else {
        server.emit("error", err, req, socket);
      }
      proxyReq.destroy();
    }

    function onOutgoingError(err: Error) {
      if (callback) {
        callback(err, req, socket);
      } else {
        server.emit("error", err, req, socket);
      }
      socket.end();
    }
  },
);

export const websocketIncomingMiddleware: readonly ProxyMiddleware<Socket>[] = [
  checkMethodAndHeader,
  XHeaders,
  stream,
] as const;


================================================
FILE: src/server.ts
================================================
import http from "node:http";
import https from "node:https";
import http2 from "node:http2";
import { EventEmitter } from "node:events";
import { webIncomingMiddleware } from "./middleware/web-incoming.ts";
import { websocketIncomingMiddleware } from "./middleware/ws-incoming.ts";
import type { ProxyServerOptions, ProxyTarget } from "./types.ts";
import type { ProxyMiddleware, ResOfType } from "./middleware/_utils.ts";
import type net from "node:net";

export interface ProxyServerEventMap<
  Req extends http.IncomingMessage | http2.Http2ServerRequest = http.IncomingMessage,
  Res extends http.ServerResponse | http2.Http2ServerResponse = http.ServerResponse,
> {
  error: [err: Error, req?: Req, res?: Res | net.Socket, target?: URL | ProxyTarget];
  start: [req: Req, res: Res, target: URL | ProxyTarget];
  econnreset: [err: Error, req: Req, res: Res, target: URL | ProxyTarget];
  proxyReq: [proxyReq: http.ClientRequest, req: Req, res: Res, options: ProxyServerOptions];
  proxyReqWs: [
    proxyReq: http.ClientRequest,
    req: Req,
    socket: net.Socket,
    options: ProxyServerOptions,
    head: any,
  ];
  proxyRes: [proxyRes: http.IncomingMessage, req: Req, res: Res];
  end: [req: Req, res: Res, proxyRes: http.IncomingMessage];
  open: [proxySocket: net.Socket];
  /** @deprecated */
  proxySocket: [proxySocket: net.Socket];
  close: [proxyRes: Req, proxySocket: net.Socket, proxyHead: any];
}

// eslint-disable-next-line unicorn/prefer-event-target
export class ProxyServer<
  Req extends http.IncomingMessage | http2.Http2ServerRequest = http.IncomingMessage,
  Res extends http.ServerResponse | http2.Http2ServerResponse = http.ServerResponse,
> extends EventEmitter<ProxyServerEventMap<Req, Res>> {
  // we use http2.Http2Server to handle HTTP/1.1 HTTPS as well (with allowHTTP1 enabled)
  private _server?: http.Server | https.Server | http2.Http2SecureServer;

  _webPasses: ProxyMiddleware<http.ServerResponse>[] = [...webIncomingMiddleware];
  _wsPasses: ProxyMiddleware<net.Socket>[] = [...websocketIncomingMiddleware];

  options: ProxyServerOptions;

  web: (req: Req, res: Res, opts?: ProxyServerOptions, head?: any) => Promise<void>;

  ws: (req: Req, socket: net.Socket, opts: ProxyServerOptions, head?: any) => Promise<void>;

  /**
   * Creates the proxy server with specified options.
   * @param options - Config object passed to the proxy
   */
  constructor(options: ProxyServerOptions = {}) {
    super();

    this.options = options || {};
    this.options.prependPath = options.prependPath !== false;

    this.web = _createProxyFn("web", this);
    this.ws = _createProxyFn("ws", this);
  }

  /**
   * A function that wraps the object in a webserver, for your convenience
   * @param port - Port to listen on
   * @param hostname - The hostname to listen on
   * @param listeningListener - A callback function that is called when the server starts listening
   */
  listen(port: number, hostname?: string, listeningListener?: () => void) {
    const closure = (
      req: http.IncomingMessage | http2.Http2ServerRequest,
      res: http.ServerResponse | http2.Http2ServerResponse,
    ) => {
      return this.web(req as Req, res as Res);
    };

    if (this.options.http2) {
      if (!this.options.ssl) {
        throw new Error("HTTP/2 requires ssl option");
      }
      this._server = http2.createSecureServer({ ...this.options.ssl, allowHTTP1: true }, closure);
    } else if (this.options.ssl) {
      this._server = https.createServer(this.options.ssl, closure);
    } else {
      this._server = http.createServer(closure);
    }

    if (this.options.ws) {
      this._server.on("upgrade", (req, socket, head) => {
        this.ws(req, socket, this.options, head).catch(() => {});
      });
    }

    this._server.listen(port, hostname, listeningListener);

    return this;
  }

  /**
   * A function that closes the inner webserver and stops listening on given port
   */
  close(callback?: () => void) {
    if (this._server) {
      // Wrap callback to nullify server after all open connections are closed.
      this._server.close((...args) => {
        this._server = undefined;
        if (callback) {
          Reflect.apply(callback, undefined, args);
        }
      });
    }
  }

  before<Type extends "ws" | "web">(
    type: Type,
    passName: string,
    pass: ProxyMiddleware<ResOfType<Type>>,
  ) {
    if (type !== "ws" && type !== "web") {
      throw new Error("type must be `web` or `ws`");
    }
    const passes = this._getPasses(type);
    let i: false | number = false;
    for (const [idx, v] of passes.entries()) {
      if (v.name === passName) {
        i = idx;
      }
    }
    if (i === false) {
      throw new Error("No such pass");
    }
    passes.splice(i, 0, pass);
  }

  after<Type extends "ws" | "web">(
    type: Type,
    passName: string,
    pass: ProxyMiddleware<ResOfType<Type>>,
  ) {
    if (type !== "ws" && type !== "web") {
      throw new Error("type must be `web` or `ws`");
    }
    const passes = this._getPasses(type);
    let i: boolean | number = false;
    for (const [idx, v] of passes.entries()) {
      if (v.name === passName) {
        i = idx;
      }
    }
    if (i === false) {
      throw new Error("No such pass");
    }
    passes.splice(i++, 0, pass);
  }

  /** @internal */
  _getPasses<Type extends "ws" | "web">(type: Type): ProxyMiddleware<ResOfType<Type>>[] {
    return (type === "ws" ? this._wsPasses : this._webPasses) as unknown as ProxyMiddleware<
      ResOfType<Type>
    >[];
  }
}

/**
 * Creates the proxy server.
 *
 * Examples:
 *
 *    httpProxy.createProxyServer({ .. }, 8000)
 *    // => '{ web: [Function], ws: [Function] ... }'
 *
 * @param {Object} Options Config object passed to the proxy
 *
 * @return {Object} Proxy Proxy object with handlers for `ws` and `web` requests
 *
 * @api public
 */
export function createProxyServer(options: ProxyServerOptions = {}) {
  return new ProxyServer(options);
}

// --- Internal ---

function _createProxyFn<
  Type extends "web" | "ws",
  ProxyServerReq extends http.IncomingMessage | http2.Http2ServerRequest,
  ProxyServerRes extends http.ServerResponse | http2.Http2ServerResponse,
>(type: Type, server: ProxyServer<ProxyServerReq, ProxyServerRes>) {
  return function (
    this: ProxyServer<ProxyServerReq, ProxyServerRes>,
    req: ProxyServerReq,
    res: ResOfType<Type>,
    opts?: ProxyServerOptions,
    head?: any,
  ): Promise<void> {
    const requestOptions = { ...opts, ...server.options };

    for (const key of ["target", "forward"] as const) {
      if (typeof requestOptions[key] === "string") {
        requestOptions[key] = new URL(requestOptions[key]);
      }
    }

    if (!requestOptions.target && !requestOptions.forward) {
      this.emit("error", new Error("Must provide a proper URL as target"));
      return Promise.resolve();
    }

    let _resolve!: () => void;
    let _reject!: (error: any) => void;
    const callbackPromise = new Promise<void>((resolve, reject) => {
      _resolve = resolve;
      _reject = reject;
    });

    res.on("close", () => {
      _resolve();
    });
    res.on("error", (error: any) => {
      _reject(error);
    });

    for (const pass of server._getPasses(type)) {
      let stop: void | true;
      try {
        stop = pass(
          req,
          res,
          requestOptions as ProxyServerOptions & { target: URL; forward: URL },
          server as ProxyServer<
            http.IncomingMessage | http2.Http2ServerRequest,
            http.ServerResponse | http2.Http2ServerResponse
          >,
          head,
          (error) => {
            if (server.listenerCount("error") > 0) {
              server.emit("error", error, req, res as ProxyServerRes | net.Socket);
              _resolve();
            } else {
              _reject(error);
            }
          },
        );
      } catch (error) {
        if (server.listenerCount("error") > 0) {
          server.emit("error", error as Error, req, res as ProxyServerRes | net.Socket);
          _resolve();
        } else {
          _reject(error);
        }
        break;
      }
      // Passes can return a truthy value to halt the loop
      if (stop) {
        _resolve();
        break;
      }
    }

    return callbackPromise;
  };
}


================================================
FILE: src/types.ts
================================================
import type * as stream from "node:stream";

export interface ProxyTargetDetailed {
  host?: string;
  port?: number | string;
  protocol?: string;
  hostname?: string;
  socketPath?: string;
  key?: string;
  passphrase?: string;
  pfx?: Buffer | string;
  cert?: string;
  ca?: string;
  ciphers?: string;
  secureProtocol?: string;
}

export type ProxyTarget = string | URL | ProxyTargetDetailed;

/** Resolved proxy address — either TCP (host + port) or Unix socket. */
export type ProxyAddr =
  | { host?: string; port: number; socketPath?: undefined }
  | { host?: undefined; port?: undefined; socketPath: string };

export interface ProxyServerOptions {
  /** URL string to be parsed. */
  target?: ProxyTarget;
  /** URL string to be parsed. */
  forward?: ProxyTarget;
  /** Object to be passed to http(s).request. */
  agent?: any;
  /** Enable HTTP/2 listener, default is `false` */
  http2?: boolean;
  /** Object to be passed to https.createServer()
   * or http2.createSecureServer() if the `http2` option is enabled
   */
  ssl?: any;
  /** If you want to proxy websockets. */
  ws?: boolean;
  /** Adds x- forward headers. */
  xfwd?: boolean;
  /** Verify SSL certificate. */
  secure?: boolean;
  /** Explicitly specify if we are proxying to another proxy. */
  toProxy?: boolean;
  /** Specify whether you want to prepend the target's path to the proxy path. */
  prependPath?: boolean;
  /** Specify whether you want to ignore the proxy path of the incoming request. */
  ignorePath?: boolean;
  /** Local interface string to bind for outgoing connections. */
  localAddress?: string;
  /** Changes the origin of the host header to the target URL. */
  changeOrigin?: boolean;
  /** specify whether you want to keep letter case of response header key */
  preserveHeaderKeyCase?: boolean;
  /** Basic authentication i.e. 'user:password' to compute an Authorization header. */
  auth?: string;
  /** Rewrites the location hostname on (301 / 302 / 307 / 308) redirects, Default: null. */
  hostRewrite?: string;
  /** Rewrites the location host/ port on (301 / 302 / 307 / 308) redirects based on requested host/ port.Default: false. */
  autoRewrite?: boolean;
  /** Rewrites the location protocol on (301 / 302 / 307 / 308) redirects to 'http' or 'https'.Default: null. */
  protocolRewrite?: string;
  /** Rewrites domain of set-cookie headers. */
  cookieDomainRewrite?: false | string | { [oldDomain: string]: string };
  /** Rewrites path of set-cookie headers. Default: false */
  cookiePathRewrite?: false | string | { [oldPath: string]: string };
  /** Object with extra headers to be added to target requests. */
  headers?: { [header: string]: string };
  /** Timeout (in milliseconds) when proxy receives no response from target. Default: 120000 (2 minutes) */
  proxyTimeout?: number;
  /** Timeout (in milliseconds) for incoming requests */
  timeout?: number;
  /** If set to true, none of the webOutgoing passes are called and it's your responsibility to appropriately return the response by listening and acting on the proxyRes event */
  selfHandleResponse?: boolean;
  /** Follow HTTP redirects from target. `true` = max 5 hops; number = custom max. */
  followRedirects?: boolean | number;
  /** Buffer */
  buffer?: stream.Stream;
}


================================================
FILE: src/ws.ts
================================================
import type { IncomingMessage, RequestOptions } from "node:http";
import { request as httpRequest } from "node:http";
import { request as httpsRequest } from "node:https";
import type { Duplex } from "node:stream";
import type { Socket } from "node:net";
import type { ProxyAddr } from "./types.ts";
import {
  getPort,
  hasEncryptedConnection,
  isSSL,
  parseAddr,
  setupOutgoing,
  setupSocket,
} from "./_utils.ts";

/**
 * Options for {@link proxyUpgrade}.
 */
export interface ProxyUpgradeOptions {
  /**
   * Add `x-forwarded-for`, `x-forwarded-port`, and `x-forwarded-proto` headers.
   * Default: `true`.
   */
  xfwd?: boolean;
  /**
   * Rewrite the `Host` header to match the target.
   * Default: `false` (original host is kept).
   */
  changeOrigin?: boolean;
  /**
   * Extra headers to include in the upstream upgrade request.
   * Default: none.
   */
  headers?: Record<string, string>;
  /**
   * TLS options forwarded to `https.request`.
   * Default: none.
   */
  ssl?: Record<string, unknown>;
  /**
   * Whether to verify upstream TLS certificates.
   * Default: `true`.
   */
  secure?: boolean;
  /**
   * HTTP/HTTPS agent used for the upstream request.
   * Default: `false` (no keep-alive agent is used).
   */
  agent?: any;
  /**
   * Local interface address to bind for upstream connections.
   * Default: OS-selected local address.
   */
  localAddress?: string;
  /**
   * Basic auth credentials in `username:password` format.
   * Default: none.
   */
  auth?: string;
  /**
   * Prepend the target path to the proxied request path.
   * Default: `true`.
   */
  prependPath?: boolean;
  /**
   * Ignore the incoming request path when building the upstream path.
   * Default: `false` (incoming path is used).
   */
  ignorePath?: boolean;
  /**
   * Send absolute URL in request path when proxying to another proxy.
   * Default: `false` (path-only request target is used).
   */
  toProxy?: boolean;
}

/**
 * Proxy a WebSocket upgrade request to a target address without creating a
 * {@link ProxyServer} instance. Similar to {@link proxyFetch} but for
 * WebSocket upgrades.
 *
 * @param addr - Target server address. Can be a URL string (`http://host:port`, `ws://host:port`, `unix:/path`), or an object with `host`/`port` for TCP or `socketPath` for Unix sockets.
 * @param req - The incoming HTTP upgrade request.
 * @param socket - The network socket between the server and client.
 * @param head - The first packet of the upgraded stream (may be empty).
 * @param opts - Optional proxy options.
 * @returns A promise that resolves with the upstream proxy socket once the
 * WebSocket connection is established, or rejects on error.
 */
export function proxyUpgrade(
  addr: string | ProxyAddr,
  req: IncomingMessage,
  socket: Duplex,
  head?: Buffer,
  opts?: ProxyUpgradeOptions,
): Promise<Socket> {
  const resolvedAddr = parseAddr(addr);

  // Detect SSL from addr string protocol (wss:// or https://)
  let useSSL = false;
  if (typeof addr === "string" && !addr.startsWith("unix:")) {
    useSSL = isSSL.test(new URL(addr).protocol);
  }

  // Validate WS upgrade request
  if (req.method !== "GET" || req.headers.upgrade?.toLowerCase() !== "websocket") {
    socket.destroy();
    return Promise.reject(new Error("Not a valid WebSocket upgrade request"));
  }

  // Set x-forwarded-* headers (enabled by default)
  if (opts?.xfwd !== false) {
    const xfFor = req.headers["x-forwarded-for"];
    const xfPort = req.headers["x-forwarded-port"];
    const xfProto = req.headers["x-forwarded-proto"];
    req.headers["x-forwarded-for"] = `${xfFor ? `${xfFor},` : ""}${req.socket?.remoteAddress}`;
    req.headers["x-forwarded-port"] = `${xfPort ? `${xfPort},` : ""}${getPort(req)}`;
    req.headers["x-forwarded-proto"] =
      `${xfProto ? `${xfProto},` : ""}${hasEncryptedConnection(req) ? "wss" : "ws"}`;
  }

  // Build target URL for setupOutgoing
  const target = _buildTargetURL(resolvedAddr, useSSL);
  const requestOptions: ProxyUpgradeOptions & { target: URL } = {
    ...opts,
    target,
    prependPath: opts?.prependPath !== false,
  };

  const outgoing = setupOutgoing(
    requestOptions.ssl || {},
    requestOptions as Parameters<typeof setupOutgoing>[1],
    req,
  );

  const sock = socket as Socket;

  return new Promise<Socket>((resolve, reject) => {
    let settled = false;

    setupSocket(sock);

    if (head && head.length > 0) {
      sock.unshift(head);
    }

    sock.once("error", onSocketError);

    const doRequest = isSSL.test(target.protocol) ? httpsRequest : httpRequest;
    const proxyReq = doRequest(outgoing as RequestOptions);

    proxyReq.once("error", onOutgoingError);

    proxyReq.once("response", (res) => {
      // If upgrade event isn't going to happen, relay the response and reject
      // Guard against writing to an already-destroyed socket
      // (https://github.com/http-party/node-http-proxy/pull/1433)
      if (!(res as any).upgrade) {
        if (!sock.destroyed && sock.writable) {
          sock.write(
            _createHttpHeader(
              `HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}`,
              res.headers,
            ),
          );
          res.on("error", onOutgoingError);
          res.pipe(sock);
        } else {
          // Socket already gone — consume response to avoid unhandled stream errors
          res.resume();
        }
        if (!settled) {
          settled = true;
          reject(new Error("Upstream server did not upgrade the connection"));
        }
      }
    });

    proxyReq.once("upgrade", (proxyRes, proxySocket, proxyHead) => {
      proxySocket.once("error", onOutgoingError);

      sock.removeListener("error", onSocketError);
      sock.once("error", () => {
        proxySocket.end();
      });

      setupSocket(proxySocket);

      if (proxyHead && proxyHead.length > 0) {
        proxySocket.unshift(proxyHead);
      }

      sock.write(_createHttpHeader("HTTP/1.1 101 Switching Protocols", proxyRes.headers));
      proxySocket.pipe(sock).pipe(proxySocket);

      settled = true;
      resolve(proxySocket);
    });

    proxyReq.end();

    function onSocketError(err: Error) {
      proxyReq.destroy();
      if (!settled) {
        settled = true;
        reject(err);
      }
    }

    function onOutgoingError(err: Error) {
      sock.end();
      if (!settled) {
        settled = true;
        reject(err);
      }
    }
  });
}

// --- Internal ---

function _buildTargetURL(addr: ProxyAddr, useSSL = false): URL {
  const protocol = useSSL ? "https" : "http";
  if (addr.socketPath) {
    const url = new URL(`${protocol}://unix`);
    (url as any).socketPath = addr.socketPath;
    return url;
  }
  return new URL(`${protocol}://${addr.host || "localhost"}${addr.port ? `:${addr.port}` : ""}`);
}

function _createHttpHeader(
  line: string,
  headers: Record<string, string | string[] | undefined>,
): string {
  let result = line;
  for (const key of Object.keys(headers)) {
    const value = headers[key];
    if (value === undefined) {
      continue;
    }
    if (Array.isArray(value)) {
      for (const element of value) {
        result += `\r\n${key}: ${element}`;
      }
    } else {
      result += `\r\n${key}: ${value}`;
    }
  }
  return `${result}\r\n\r\n`;
}


================================================
FILE: test/_stubs.ts
================================================
import type {
  IncomingMessage,
  OutgoingHttpHeaders,
  RequestOptions,
  ServerResponse,
} from "node:http";
import type { RequestOptions as HttpsRequestOptions } from "node:https";
import type { Socket } from "node:net";
import type { ProxyServer } from "../src/server.ts";
import type { ProxyServerOptions, ProxyTargetDetailed } from "../src/types.ts";

// --- setupOutgoing ---

export type OutgoingOptions = Omit<RequestOptions & HttpsRequestOptions, "headers"> & {
  headers?: OutgoingHttpHeaders & Record<string, unknown>;
};

export function createOutgoing(): OutgoingOptions {
  return {};
}

// --- IncomingMessage stubs ---

export function stubIncomingMessage(overrides: Record<string, unknown> = {}): IncomingMessage {
  return {
    method: "GET",
    url: "/",
    headers: {},
    httpVersion: "1.1",
    httpVersionMajor: 1,
    ...overrides,
  } as unknown as IncomingMessage;
}

// --- ServerResponse stub ---

export function stubServerResponse(overrides: Record<string, unknown> = {}): ServerResponse {
  return overrides as unknown as ServerResponse;
}

// --- Socket stub ---

export function stubSocket(overrides: Record<string, unknown> = {}): Socket {
  return overrides as unknown as Socket;
}

// --- Middleware options ---

export type MiddlewareOptions = ProxyServerOptions & {
  target: URL | ProxyTargetDetailed;
  forward: URL;
};

export function stubMiddlewareOptions(overrides: Record<string, unknown> = {}): MiddlewareOptions {
  return overrides as unknown as MiddlewareOptions;
}

// --- ProxyServer stub ---

export function stubProxyServer(overrides: Record<string, unknown> = {}): ProxyServer<any, any> {
  return overrides as unknown as ProxyServer;
}


================================================
FILE: test/_utils.test.ts
================================================
import { describe, it, expect } from "vitest";
import * as common from "../src/_utils.ts";
import { createOutgoing, stubIncomingMessage, stubSocket } from "./_stubs.ts";

// Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-http-proxy-common-test.js

// Difference from http-proxy is that we always ensure leading slash on path

describe("lib/http-proxy/common.js", () => {
  describe("#setupOutgoing", () => {
    it("should setup the correct headers", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          agent: "?",
          target: {
            host: "hey",
            hostname: "how",
            socketPath: "are",
            port: "you",
          },
          // @ts-expect-error
          headers: { fizz: "bang", overwritten: true },
          localAddress: "local.address",
          auth: "username:pass",
        },
        stubIncomingMessage({
          method: "i",
          url: "am",
          headers: { pro: "xy", overwritten: "false" },
        }),
      );

      expect(outgoing.host).to.eql("hey");
      expect(outgoing.hostname).to.eql("how");
      expect(outgoing.socketPath).to.eql("are");
      expect(outgoing.port).to.eql("you");
      expect(outgoing.agent).to.eql("?");

      expect(outgoing.method).to.eql("i");
      expect(outgoing.path).to.eql("/am"); // leading slash is new in httpxy

      expect(outgoing.headers!.pro).to.eql("xy");
      expect(outgoing.headers!.fizz).to.eql("bang");
      expect(outgoing.headers!.overwritten).to.eql(true);
      expect(outgoing.localAddress).to.eql("local.address");
      expect(outgoing.auth).to.eql("username:pass");
    });

    it("should not overwrite existing ssl options with undefined target values", () => {
      const outgoing = createOutgoing();
      // Simulate options.ssl having cert/key/ca (as passed from web-incoming)
      Object.assign(outgoing, {
        cert: "my-cert",
        key: "my-key",
        ca: "my-ca",
      });
      common.setupOutgoing(
        outgoing,
        {
          agent: undefined,
          target: {
            host: "localhost",
            hostname: "localhost",
            port: "8080",
            // No SSL properties on target — they should NOT overwrite outgoing
          },
        },
        stubIncomingMessage({
          method: "GET",
          url: "/",
          headers: {},
        }),
      );
      // SSL options from outgoing (options.ssl) must be preserved
      expect(outgoing.cert).to.eql("my-cert");
      expect(outgoing.key).to.eql("my-key");
      expect(outgoing.ca).to.eql("my-ca");
    });

    it("should not override agentless upgrade header", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          agent: undefined,
          target: {
            host: "hey",
            hostname: "how",
            socketPath: "are",
            port: "you",
          },
          headers: { connection: "upgrade" },
        },
        stubIncomingMessage({
          method: "i",
          url: "am",
          headers: { pro: "xy", overwritten: "false" },
        }),
      );
      expect(outgoing.headers!.connection).to.eql("upgrade");
    });

    it("should not override agentless connection: contains upgrade", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          agent: undefined,
          target: {
            host: "hey",
            hostname: "how",
            socketPath: "are",
            port: "you",
          },
          headers: { connection: "keep-alive, upgrade" }, // this is what Firefox sets
        },
        stubIncomingMessage({
          method: "i",
          url: "am",
          headers: { pro: "xy", overwritten: "false" },
        }),
      );
      expect(outgoing.headers!.connection).to.eql("keep-alive, upgrade");
    });

    it("should override agentless connection: contains improper upgrade", () => {
      // sanity check on upgrade regex
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          agent: false,
          target: {
            host: "hey",
            hostname: "how",
            socketPath: "are",
            port: "you",
          },
          headers: { connection: "keep-alive, not upgrade" },
        },
        stubIncomingMessage({
          method: "i",
          url: "am",
          headers: { pro: "xy", overwritten: "false" },
        }),
      );
      expect(outgoing.headers!.connection).to.eql("close");
    });

    it("should override agentless non-upgrade header to close", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          agent: false,
          target: {
            host: "hey",
            hostname: "how",
            socketPath: "are",
            port: "you",
          },
          headers: { connection: "xyz" },
        },
        stubIncomingMessage({
          method: "i",
          url: "am",
          headers: { pro: "xy", overwritten: "false" },
        }),
      );
      expect(outgoing.headers!.connection).to.eql("close");
    });

    it("should use default keep-alive agent if none is given", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        { target: "http://localhost" },
        stubIncomingMessage({
          url: "/",
        }),
      );
      expect(outgoing.agent).to.eql(common.defaultAgents.http);
    });

    it("should set the agent to false if explicitly set", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        { target: "http://localhost", agent: false },
        stubIncomingMessage({
          url: "/",
        }),
      );
      expect(outgoing.agent).to.eql(false);
    });

    it("should not use keep-alive agent for websocket upgrade requests", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        { target: "http://localhost" },
        stubIncomingMessage({
          url: "/",
          headers: { connection: "Upgrade", upgrade: "websocket" },
        }),
      );
      // WebSocket upgrades take ownership of the socket after 101,
      // so a keep-alive agent must not be used.
      expect(outgoing.agent).to.eql(false);
    });

    it("set the port according to the protocol", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          agent: "?",
          target: {
            host: "how",
            hostname: "are",
            socketPath: "you",
            protocol: "https:",
          },
        },
        stubIncomingMessage({
          method: "i",
          url: "am",
          headers: { pro: "xy" },
        }),
      );

      expect(outgoing.host).to.eql("how");
      expect(outgoing.hostname).to.eql("are");
      expect(outgoing.socketPath).to.eql("you");
      expect(outgoing.agent).to.eql("?");

      expect(outgoing.method).to.eql("i");
      expect(outgoing.path).to.eql("/am");
      expect(outgoing.headers!.pro).to.eql("xy");

      expect(outgoing.port).to.eql(443);
    });

    it("should keep the original target path in the outgoing path", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        { target: URL.parse("http://localhost/some-path")! },
        stubIncomingMessage({
          url: "am",
        }),
      );

      expect(outgoing.path).to.eql("/some-path/am");
    });

    it("should keep the original forward path in the outgoing path", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          target: "http://localhost",
          forward: URL.parse("http://localhost/some-path")!,
        },
        stubIncomingMessage({
          url: "am",
        }),
        "forward",
      );

      expect(outgoing.path).to.eql("/some-path/am");
    });

    it("should properly detect https/wss protocol without the colon", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          target: {
            protocol: "https",
            host: "whatever.com",
          },
        },
        stubIncomingMessage({ url: "/" }),
      );

      expect(outgoing.port).to.eql(443);
    });

    it("should not prepend the target path to the outgoing path with prependPath = false", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          target: URL.parse("http://localhost/hellothere")!,
          prependPath: false,
        },
        stubIncomingMessage({ url: "hi" }),
      );

      expect(outgoing.path).to.eql("/hi");
    });

    it("should properly join paths", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          target: URL.parse("http://localhost/forward")!,
        },
        stubIncomingMessage({ url: "/static/path" }),
      );

      expect(outgoing.path).to.eql("/forward/static/path");
    });

    it("should preserve multiple consecutive slashes in path (#80)", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        { target: "http://localhost:3004" },
        stubIncomingMessage({ url: "//test" }),
      );
      expect(outgoing.path).to.eql("//test");
    });

    it("should preserve multiple consecutive slashes with query string (#80)", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        { target: "http://localhost:3004" },
        stubIncomingMessage({
          url: "//test?foo=bar",
        }),
      );
      expect(outgoing.path).to.eql("//test?foo=bar");
    });

    it("should preserve target query string when merging paths", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          target: URL.parse("http://localhost/api?key=val")!,
        },
        stubIncomingMessage({ url: "/endpoint" }),
      );

      expect(outgoing.path).to.eql("/api/endpoint?key=val");
    });

    it("should merge target and request query strings with &", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          target: URL.parse("http://localhost/api?key=val")!,
        },
        stubIncomingMessage({ url: "/endpoint?foo=bar" }),
      );

      expect(outgoing.path).to.eql("/api/endpoint?key=val&foo=bar");
    });

    it("should preserve target query string with no request path", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          target: URL.parse("http://localhost?project=example&path=")!,
        },
        stubIncomingMessage({ url: "/" }),
      );

      expect(outgoing.path).to.eql("/?project=example&path=");
    });

    it("should not modify the query string", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          target: URL.parse("http://localhost/forward")!,
        },
        stubIncomingMessage({
          url: "/?foo=bar//&target=http://foobar.com/?a=1%26b=2&other=2",
        }),
      );

      expect(outgoing.path).to.eql(
        "/forward/?foo=bar//&target=http://foobar.com/?a=1%26b=2&other=2",
      );
    });

    //
    // This is the proper failing test case for the common.join problem
    //
    it("should correctly format the toProxy URL", () => {
      const outgoing = createOutgoing();
      const google = "https://google.com";
      common.setupOutgoing(
        outgoing,
        {
          target: URL.parse("http://sometarget.com:80")!,
          toProxy: true,
        },
        stubIncomingMessage({ url: google }),
      );

      expect(outgoing.path).to.eql("/" + google);
    });

    it("should not replace :\\ to :\\\\ when no https word before", () => {
      const outgoing = createOutgoing();
      const google = "https://google.com:/join/join.js";
      common.setupOutgoing(
        outgoing,
        {
          target: URL.parse("http://sometarget.com:80")!,
          toProxy: true,
        },
        stubIncomingMessage({ url: google }),
      );

      expect(outgoing.path).to.eql("/" + google);
    });

    it("should not replace :\\ to \\\\ when no http word before", () => {
      const outgoing = createOutgoing();
      const google = "http://google.com:/join/join.js";
      common.setupOutgoing(
        outgoing,
        {
          target: URL.parse("http://sometarget.com:80")!,
          toProxy: true,
        },
        stubIncomingMessage({ url: google }),
      );

      expect(outgoing.path).to.eql("/" + google);
    });

    describe("when using ignorePath", () => {
      it("should ignore the path of the `req.url` passed in but use the target path", () => {
        const outgoing = createOutgoing();
        const myEndpoint = "https://whatever.com/some/crazy/path/whoooo";
        common.setupOutgoing(
          outgoing,
          {
            target: URL.parse(myEndpoint)!,
            ignorePath: true,
          },
          stubIncomingMessage({ url: "/more/crazy/pathness" }),
        );

        expect(outgoing.path).to.eql("/some/crazy/path/whoooo");
      });

      it("and prependPath: false, it should ignore path of target and incoming request", () => {
        const outgoing = createOutgoing();
        const myEndpoint = "https://whatever.com/some/crazy/path/whoooo";
        common.setupOutgoing(
          outgoing,
          {
            target: URL.parse(myEndpoint)!,
            ignorePath: true,
            prependPath: false,
          },
          stubIncomingMessage({ url: "/more/crazy/pathness" }),
        );

        expect(outgoing.path).to.eql("/");
      });
    });

    describe("when using changeOrigin", () => {
      it("should correctly set the port to the host when it is a non-standard port using URL.parse", () => {
        const outgoing = createOutgoing();
        const myEndpoint = "https://myCouch.com:6984";
        common.setupOutgoing(
          outgoing,
          {
            target: URL.parse(myEndpoint)!,
            changeOrigin: true,
          },
          stubIncomingMessage({ url: "/" }),
        );

        expect(outgoing.headers!.host).to.eql("mycouch.com:6984");
      });

      it("should correctly set the port to the host when it is a non-standard port when setting host and port manually (which ignores port)", () => {
        const outgoing = createOutgoing();
        common.setupOutgoing(
          outgoing,
          {
            target: {
              protocol: "https:",
              host: "mycouch.com",
              port: 6984,
            },
            changeOrigin: true,
          },
          stubIncomingMessage({ url: "/" }),
        );
        expect(outgoing.headers!.host).to.eql("mycouch.com:6984");
      });
    });

    it("should pass through https client parameters", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          agent: "?",
          target: {
            host: "how",
            hostname: "are",
            socketPath: "you",
            protocol: "https:",
            pfx: "my-pfx",
            key: "my-key",
            passphrase: "my-passphrase",
            cert: "my-cert",
            ca: "my-ca",
            ciphers: "my-ciphers",
            secureProtocol: "my-secure-protocol",
          },
        },
        stubIncomingMessage({
          method: "i",
          url: "am",
        }),
      );

      expect(outgoing.pfx).eql("my-pfx");
      expect(outgoing.key).eql("my-key");
      expect(outgoing.passphrase).eql("my-passphrase");
      expect(outgoing.cert).eql("my-cert");
      expect(outgoing.ca).eql("my-ca");
      expect(outgoing.ciphers).eql("my-ciphers");
      expect(outgoing.secureProtocol).eql("my-secure-protocol");
    });

    it("should set ca from top-level options", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          target: { host: "localhost", protocol: "https:" },
          ca: "my-top-level-ca",
        } as any,
        stubIncomingMessage({ url: "/" }),
      );
      expect(outgoing.ca).eql("my-top-level-ca");
    });

    it("should handle overriding the `method` of the http request", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        {
          target: "https://whooooo.com",
          method: "POST",
        },
        stubIncomingMessage({ method: "GET", url: "" }),
      );

      expect(outgoing.method).eql("POST");
    });

    it("should handle empty pathname target", () => {
      const outgoing = createOutgoing();
      common.setupOutgoing(
        outgoing,
        { target: URL.parse("http://localhost")! },
        stubIncomingMessage({
          url: "",
        }),
      );

      expect(outgoing.path).toBe("/");
    });
  });

  describe("#joinURL", () => {
    it("should insert slash when base has no trailing slash and path has no leading slash", () => {
      expect(common.joinURL("foo", "bar")).to.eql("foo/bar");
    });

    it("should return path when base is undefined", () => {
      expect(common.joinURL(undefined, "/path")).to.eql("/path");
    });

    it("should return base when path is undefined", () => {
      expect(common.joinURL("/base", undefined)).to.eql("/base");
    });

    it("should return / when both are undefined", () => {
      expect(common.joinURL(undefined, undefined)).to.eql("/");
    });

    it("should strip duplicate slash when both have slash", () => {
      expect(common.joinURL("/base/", "/path")).to.eql("/base/path");
    });

    it("should concat when base has trailing slash and path has no leading slash", () => {
      expect(common.joinURL("/base/", "path")).to.eql("/base/path");
    });
  });

  describe("#rewriteCookieProperty", () => {
    it("should return original cookie when domain is not in config and no wildcard", () => {
      const cookie = "hello; domain=other.com; path=/";
      const result = common.rewriteCookieProperty(cookie, { "specific.com": "new.com" }, "domain");
      expect(result).to.eql("hello; domain=other.com; path=/");
    });
  });

  describe("#requiresPort", () => {
    it("should return false for ftp on port 21", () => {
      expect(common.requiresPort(21, "ftp")).to.eql(false);
    });

    it("should return true for ftp on non-standard port", () => {
      expect(common.requiresPort(8021, "ftp")).to.eql(true);
    });

    it("should return false for gopher on port 70", () => {
      expect(common.requiresPort(70, "gopher")).to.eql(false);
    });

    it("should return true for gopher on non-standard port", () => {
      expect(common.requiresPort(8070, "gopher")).to.eql(true);
    });

    it("should return false for file protocol", () => {
      expect(common.requiresPort(0, "file")).to.eql(false);
      expect(common.requiresPort(8080, "file")).to.eql(false);
    });

    it("should return false for unknown protocol on port 0", () => {
      expect(common.requiresPort(0, "unknown")).to.eql(false);
    });

    it("should return true for unknown protocol on non-zero port", () => {
      expect(common.requiresPort(8080, "unknown")).to.eql(true);
    });

    it("should return false when port is falsy", () => {
      expect(common.requiresPort(0, "http")).to.eql(false);
    });

    it("should handle protocol with colon", () => {
      expect(common.requiresPort(80, "http:")).to.eql(false);
      expect(common.requiresPort(8080, "http:")).to.eql(true);
    });
  });

  describe("#parseAddr", () => {
    it("should default to port 80 for http", () => {
      expect(common.parseAddr("http://localhost")).to.eql({ host: "localhost", port: 80 });
    });

    it("should default to port 443 for https", () => {
      expect(common.parseAddr("https://localhost")).to.eql({ host: "localhost", port: 443 });
    });

    it("should default to port 443 for wss", () => {
      expect(common.parseAddr("wss://localhost")).to.eql({ host: "localhost", port: 443 });
    });

    it("should default to port 80 for ws", () => {
      expect(common.parseAddr("ws://localhost")).to.eql({ host: "localhost", port: 80 });
    });

    it("should use explicit port over protocol default", () => {
      expect(common.parseAddr("https://localhost:8443")).to.eql({ host: "localhost", port: 8443 });
    });

    it("should parse unix socket path", () => {
      expect(common.parseAddr("unix:/tmp/sock")).to.eql({ socketPath: "/tmp/sock" });
    });

    it("should pass through a valid ProxyAddr with port", () => {
      const addr = { host: "127.0.0.1", port: 3000 };
      expect(common.parseAddr(addr)).to.eql(addr);
    });

    it("should pass through a valid ProxyAddr with socketPath", () => {
      const addr = { socketPath: "/tmp/proxy.sock" };
      expect(common.parseAddr(addr)).to.eql(addr);
    });

    it("should throw for ProxyAddr missing port and socketPath", () => {
      expect(() => common.parseAddr({} as any)).toThrowError(
        /ProxyAddr must have either `port` or `socketPath`/,
      );
    });
  });

  describe("#setupSocket", () => {
    it("should setup a socket", () => {
      const socketConfig = {
          timeout: undefined as number | undefined,
          nodelay: false,
          keepalive: false,
        },
        sock = stubSocket({
          setTimeout: function (num: number) {
            socketConfig.timeout = num;
          },
          setNoDelay: function (bol: boolean) {
            socketConfig.nodelay = bol;
          },
          setKeepAlive: function (bol: boolean) {
            socketConfig.keepalive = bol;
          },
        });
      const returnValue = common.setupSocket(sock);

      expect(socketConfig.timeout).to.eql(0);
      expect(socketConfig.nodelay).to.eql(true);
      expect(socketConfig.keepalive).to.eql(true);
    });
  });
});


================================================
FILE: test/_utils.ts
================================================
import http from "node:http";
import https from "node:https";
import net from "node:net";
import type { AddressInfo } from "node:net";
import * as httpProxy from "../src/index.ts";

export function listenOn(server: http.Server | https.Server | net.Server): Promise<number> {
  return new Promise((resolve, reject) => {
    server.once("error", reject);
    server.listen(0, "127.0.0.1", () => {
      resolve((server.address() as AddressInfo).port);
    });
  });
}

export function proxyListen(
  proxy: ReturnType<typeof httpProxy.createProxyServer>,
): Promise<number> {
  return new Promise((resolve, reject) => {
    proxy.listen(0, "127.0.0.1");
    const server = (proxy as any)._server as net.Server;
    server.once("error", reject);
    server.once("listening", () => {
      resolve((server.address() as AddressInfo).port);
    });
  });
}


================================================
FILE: test/fetch.test.ts
================================================
import { createServer, type Server } from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";

import type { AddressInfo } from "node:net";

import { proxyFetch } from "../src/fetch.ts";

// --- TCP server ---

let tcpServer: Server;
let tcpPort: number;

beforeAll(async () => {
  tcpServer = createServer((req, res) => {
    if (req.url === "/json") {
      res.writeHead(200, { "content-type": "application/json" });
      res.end(JSON.stringify({ ok: true }));
      return;
    }
    if (req.url === "/slow") {
      const timer = setTimeout(() => {
        res.writeHead(200, { "content-type": "text/plain" });
        res.end("slow-ok");
      }, 5000);
      req.on("close", () => clearTimeout(timer));
      return;
    }
    if (req.url === "/echo" && req.method === "POST") {
      const chunks: Buffer[] = [];
      req.on("data", (c) => chunks.push(c));
      req.on("end", () => {
        res.writeHead(200, { "content-type": "text/plain" });
        res.end(Buffer.concat(chunks));
      });
      return;
    }
    if (req.url?.startsWith("/headers")) {
      res.writeHead(200, { "content-type": "application/json" });
      res.end(JSON.stringify({ headers: req.headers, url: req.url }));
      return;
    }
    if (req.url === "/redirect") {
      res.writeHead(302, { location: "/json" });
      res.end();
      return;
    }
    if (req.url === "/redirect-307") {
      res.writeHead(307, { location: "/echo" });
      res.end();
      return;
    }
    if (req.url === "/redirect-chain") {
      res.writeHead(301, { location: "/redirect" });
      res.end();
      return;
    }
    if (req.url === "/multi-cookie") {
      res.writeHead(200, [
        ["set-cookie", "a=1; Path=/"],
        ["set-cookie", "b=2; Path=/"],
        ["set-cookie", "c=3; Path=/"],
        ["content-type", "text/plain"],
      ]);
      res.end("ok");
      return;
    }
    if (req.url === "/no-content") {
      res.writeHead(204);
      res.end();
      return;
    }
    res.writeHead(404);
    res.end("Not found");
  });

  await new Promise<void>((resolve) => {
    tcpServer.listen(0, "127.0.0.1", resolve);
  });
  tcpPort = (tcpServer.address() as AddressInfo).port;
});

afterAll(() => {
  tcpServer?.close();
});

// --- Unix socket server ---

let socketServer: Server;
const socketPath = join(tmpdir(), `httpxy-test-${process.pid}-${Date.now()}.sock`);

beforeAll(async () => {
  socketServer = createServer((req, res) => {
    res.writeHead(200, { "content-type": "text/plain" });
    res.end("unix-ok");
  });
  await new Promise<void>((resolve) => {
    socketServer.listen(socketPath, resolve);
  });
});

afterAll(() => {
  socketServer?.close();
});

// --- Tests ---

describe("proxyFetch", () => {
  describe("TCP (host + port)", () => {
    it("GET request returns JSON", async () => {
      const res = await proxyFetch({ host: "127.0.0.1", port: tcpPort }, `http://localhost/json`);
      expect(res.status).toBe(200);
      expect(res.headers.get("content-type")).toBe("application/json");
      expect(await res.json()).toEqual({ ok: true });
    });

    it("POST with body", async () => {
      const res = await proxyFetch({ host: "127.0.0.1", port: tcpPort }, `http://localhost/echo`, {
        method: "POST",
        body: "hello",
      });
      expect(res.status).toBe(200);
      expect(await res.text()).toBe("hello");
    });

    it("forwards custom headers", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/headers`,
        { headers: { "x-custom": "test-value" } },
      );
      const body = (await res.json()) as { headers: Record<string, string> };
      expect(body.headers["x-custom"]).toBe("test-value");
    });

    it("handles redirect manually (no follow)", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/redirect`,
      );
      expect(res.status).toBe(302);
      expect(res.headers.get("location")).toBe("/json");
    });

    it("handles 204 no content", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/no-content`,
      );
      expect(res.status).toBe(204);
      expect(res.body).toBeNull();
    });

    it("preserves multiple set-cookie headers", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/multi-cookie`,
      );
      expect(res.status).toBe(200);
      const cookies = res.headers.getSetCookie();
      expect(cookies).toEqual(["a=1; Path=/", "b=2; Path=/", "c=3; Path=/"]);
    });

    it("strips hop-by-hop headers", async () => {
      const res = await proxyFetch({ host: "127.0.0.1", port: tcpPort }, `http://localhost/json`);
      expect(res.headers.has("transfer-encoding")).toBe(false);
      expect(res.headers.has("keep-alive")).toBe(false);
      expect(res.headers.has("connection")).toBe(false);
    });

    it("handles Request object input", async () => {
      const req = new Request("http://localhost/json", {
        headers: { accept: "application/json" },
      });
      const res = await proxyFetch({ host: "127.0.0.1", port: tcpPort }, req);
      expect(res.status).toBe(200);
      expect(await res.json()).toEqual({ ok: true });
    });

    it("preserves query string", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/headers?foo=bar`,
      );
      expect(res.status).toBe(200);
      const body = (await res.json()) as { url: string };
      expect(body.url).toBe("/headers?foo=bar");
    });
  });

  describe("Unix socket", () => {
    it("GET via unix socket", async () => {
      const res = await proxyFetch({ socketPath }, `http://localhost/anything`);
      expect(res.status).toBe(200);
      expect(await res.text()).toBe("unix-ok");
    });
  });

  describe("POST with streaming body", () => {
    it("pipes ReadableStream body", async () => {
      const stream = new ReadableStream({
        start(controller) {
          controller.enqueue(new TextEncoder().encode("streamed"));
          controller.close();
        },
      });
      const res = await proxyFetch({ host: "127.0.0.1", port: tcpPort }, `http://localhost/echo`, {
        method: "POST",
        body: stream,
      });
      expect(res.status).toBe(200);
      expect(await res.text()).toBe("streamed");
    });
  });

  describe("Request as inputInit", () => {
    it("uses Request object as inputInit", async () => {
      const initReq = new Request("http://localhost/echo", {
        method: "POST",
        body: "from-request-init",
      });
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/echo`,
        initReq,
      );
      expect(res.status).toBe(200);
      expect(await res.text()).toBe("from-request-init");
    });

    it("inputInit Request overrides input Request properties", async () => {
      const input = new Request("http://localhost/echo", { method: "GET" });
      const initReq = new Request("http://localhost/echo", {
        method: "POST",
        headers: { "x-from": "init-request" },
        body: "override-body",
      });
      const res = await proxyFetch({ host: "127.0.0.1", port: tcpPort }, input, initReq);
      expect(res.status).toBe(200);
      expect(await res.text()).toBe("override-body");
    });

    it("inputInit Request with streaming body", async () => {
      const stream = new ReadableStream({
        start(controller) {
          controller.enqueue(new TextEncoder().encode("init-stream"));
          controller.close();
        },
      });
      const initReq = new Request("http://localhost/echo", {
        method: "POST",
        body: stream,
        // @ts-expect-error duplex
        duplex: "half",
      });
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/echo`,
        initReq,
      );
      expect(res.status).toBe(200);
      expect(await res.text()).toBe("init-stream");
    });
  });

  describe("Request input with body", () => {
    it("POST body from Request input", async () => {
      const req = new Request("http://localhost/echo", {
        method: "POST",
        body: "request-body",
      });
      const res = await proxyFetch({ host: "127.0.0.1", port: tcpPort }, req);
      expect(res.status).toBe(200);
      expect(await res.text()).toBe("request-body");
    });

    it("POST streaming body from Request input", async () => {
      const stream = new ReadableStream({
        start(controller) {
          controller.enqueue(new TextEncoder().encode("req-stream"));
          controller.close();
        },
      });
      const req = new Request("http://localhost/echo", {
        method: "POST",
        body: stream,
        // @ts-expect-error duplex
        duplex: "half",
      });
      const res = await proxyFetch({ host: "127.0.0.1", port: tcpPort }, req);
      expect(res.status).toBe(200);
      expect(await res.text()).toBe("req-stream");
    });
  });

  describe("string addr", () => {
    it("accepts http URL string as addr", async () => {
      const res = await proxyFetch(`http://127.0.0.1:${tcpPort}`, `http://localhost/json`);
      expect(res.status).toBe(200);
      expect(await res.json()).toEqual({ ok: true });
    });

    it("accepts unix: string as addr", async () => {
      const res = await proxyFetch(`unix:${socketPath}`, `http://localhost/anything`);
      expect(res.status).toBe(200);
      expect(await res.text()).toBe("unix-ok");
    });
  });

  describe("error handling", () => {
    it("rejects on connection error", async () => {
      await expect(
        proxyFetch({ host: "127.0.0.1", port: 1 }, `http://localhost/`),
      ).rejects.toThrow();
    });

    it("rejects on body stream error", async () => {
      const stream = new ReadableStream({
        start(controller) {
          controller.error(new Error("stream-fail"));
        },
      });
      await expect(
        proxyFetch({ host: "127.0.0.1", port: tcpPort }, `http://localhost/echo`, {
          method: "POST",
          body: stream,
        }),
      ).rejects.toThrow("stream-fail");
    });
  });

  describe("signal / abort", () => {
    it("aborts request with already-aborted signal", async () => {
      const controller = new AbortController();
      controller.abort();
      await expect(
        proxyFetch({ host: "127.0.0.1", port: tcpPort }, `http://localhost/json`, {
          signal: controller.signal,
        }),
      ).rejects.toThrow();
    });

    it("aborts in-flight request", async () => {
      const controller = new AbortController();
      setTimeout(() => controller.abort(), 50);
      await expect(
        proxyFetch({ host: "127.0.0.1", port: tcpPort }, `http://localhost/slow`, {
          signal: controller.signal,
        }),
      ).rejects.toThrow();
    });

    it("succeeds when signal is not aborted", async () => {
      const controller = new AbortController();
      const res = await proxyFetch({ host: "127.0.0.1", port: tcpPort }, `http://localhost/json`, {
        signal: controller.signal,
      });
      expect(res.status).toBe(200);
    });
  });

  describe("timeout", () => {
    it("rejects when upstream does not respond in time", async () => {
      await expect(
        proxyFetch({ host: "127.0.0.1", port: tcpPort }, `http://localhost/slow`, undefined, {
          timeout: 50,
        }),
      ).rejects.toThrow("Proxy request timed out");
    });

    it("succeeds when response arrives before timeout", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/json`,
        undefined,
        { timeout: 5000 },
      );
      expect(res.status).toBe(200);
    });
  });

  describe("xfwd", () => {
    it("adds x-forwarded-* headers when enabled", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://example.com:3000/headers`,
        undefined,
        { xfwd: true },
      );
      const body = (await res.json()) as { headers: Record<string, string> };
      expect(body.headers["x-forwarded-for"]).toBe("example.com");
      expect(body.headers["x-forwarded-port"]).toBe("3000");
      expect(body.headers["x-forwarded-proto"]).toBe("http");
      expect(body.headers["x-forwarded-host"]).toBe("example.com:3000");
    });

    it("does not add x-forwarded-* headers by default", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/headers`,
      );
      const body = (await res.json()) as { headers: Record<string, string> };
      expect(body.headers["x-forwarded-for"]).toBeUndefined();
      expect(body.headers["x-forwarded-port"]).toBeUndefined();
      expect(body.headers["x-forwarded-proto"]).toBeUndefined();
      expect(body.headers["x-forwarded-host"]).toBeUndefined();
    });

    it("does not overwrite existing x-forwarded-* headers", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://example.com/headers`,
        { headers: { "x-forwarded-for": "10.0.0.1" } },
        { xfwd: true },
      );
      const body = (await res.json()) as { headers: Record<string, string> };
      expect(body.headers["x-forwarded-for"]).toBe("10.0.0.1");
    });
  });

  describe("changeOrigin", () => {
    it("rewrites Host header to target address", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://original-host.com/headers`,
        undefined,
        { changeOrigin: true },
      );
      const body = (await res.json()) as { headers: Record<string, string> };
      expect(body.headers.host).toBe(`127.0.0.1:${tcpPort}`);
    });

    it("keeps original Host header when changeOrigin is false", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://original-host.com/headers`,
        { headers: { host: "original-host.com" } },
      );
      const body = (await res.json()) as { headers: Record<string, string> };
      expect(body.headers.host).toBe("original-host.com");
    });

    it("uses localhost for unix socket targets", async () => {
      const unixHeaders = createServer((req, res) => {
        res.writeHead(200, { "content-type": "application/json" });
        res.end(JSON.stringify({ host: req.headers.host }));
      });
      const tmpSocket = join(tmpdir(), `httpxy-co-${process.pid}-${Date.now()}.sock`);
      await new Promise<void>((resolve) => unixHeaders.listen(tmpSocket, resolve));
      try {
        const res = await proxyFetch(
          { socketPath: tmpSocket },
          `http://original-host.com/`,
          undefined,
          { changeOrigin: true },
        );
        const body = (await res.json()) as { host: string };
        expect(body.host).toBe("localhost");
      } finally {
        unixHeaders.close();
      }
    });
  });

  describe("agent", () => {
    it("uses provided agent for connection pooling", async () => {
      const { Agent } = await import("node:http");
      const agent = new Agent({ keepAlive: true });
      try {
        const res = await proxyFetch(
          { host: "127.0.0.1", port: tcpPort },
          `http://localhost/json`,
          undefined,
          { agent },
        );
        expect(res.status).toBe(200);
        expect(await res.json()).toEqual({ ok: true });
      } finally {
        agent.destroy();
      }
    });
  });

  describe("body types", () => {
    it("sends ArrayBuffer body", async () => {
      const buf = new TextEncoder().encode("arraybuffer-body");
      const res = await proxyFetch({ host: "127.0.0.1", port: tcpPort }, `http://localhost/echo`, {
        method: "POST",
        body: buf.buffer,
      });
      expect(res.status).toBe(200);
      expect(await res.text()).toBe("arraybuffer-body");
    });

    it("sends Uint8Array body", async () => {
      const buf = new TextEncoder().encode("uint8-body");
      const res = await proxyFetch({ host: "127.0.0.1", port: tcpPort }, `http://localhost/echo`, {
        method: "POST",
        body: buf,
      });
      expect(res.status).toBe(200);
      expect(await res.text()).toBe("uint8-body");
    });

    it("sends Blob body", async () => {
      const blob = new Blob(["blob-body"], { type: "text/plain" });
      const res = await proxyFetch({ host: "127.0.0.1", port: tcpPort }, `http://localhost/echo`, {
        method: "POST",
        body: blob,
      });
      expect(res.status).toBe(200);
      expect(await res.text()).toBe("blob-body");
    });
  });

  describe("multi-value request headers", () => {
    it("preserves multiple cookie values", async () => {
      const headers = new Headers();
      headers.append("x-multi", "val1");
      headers.append("x-multi", "val2");
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/headers`,
        { headers },
      );
      const body = (await res.json()) as { headers: Record<string, string> };
      // Node.js http server joins multi-value headers with ", "
      expect(body.headers["x-multi"]).toBe("val1, val2");
    });
  });

  describe("followRedirects", () => {
    it("follows 302 redirect and returns final response", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/redirect`,
        undefined,
        { followRedirects: true },
      );
      expect(res.status).toBe(200);
      expect(await res.json()).toEqual({ ok: true });
    });

    it("follows redirect chain", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/redirect-chain`,
        undefined,
        { followRedirects: true },
      );
      expect(res.status).toBe(200);
      expect(await res.json()).toEqual({ ok: true });
    });

    it("preserves method and body on 307 redirect", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/redirect-307`,
        { method: "POST", body: "preserved" },
        { followRedirects: true },
      );
      expect(res.status).toBe(200);
      expect(await res.text()).toBe("preserved");
    });

    it("respects custom max redirects", async () => {
      // redirect-chain → redirect → json (2 hops), limit to 1
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/redirect-chain`,
        undefined,
        { followRedirects: 1 },
      );
      // After 1 hop we land on /redirect which is a 302 — returned as-is
      expect(res.status).toBe(302);
    });

    it("returns raw 3xx when followRedirects is false", async () => {
      const res = await proxyFetch(
        { host: "127.0.0.1", port: tcpPort },
        `http://localhost/redirect`,
        undefined,
        { followRedirects: false },
      );
      expect(res.status).toBe(302);
      expect(res.headers.get("location")).toBe("/json");
    });
  });

  describe("path merging", () => {
    it("prepends addr base path to request path", async () => {
      const res = await proxyFetch(
        `http://127.0.0.1:${tcpPort}/headers`,
        `http://localhost/?from=merge`,
      );
      expect(res.status).toBe(200);
      const body = (await res.json()) as { url: string };
      expect(body.url).toBe("/headers/?from=merge");
    });

    it("joins addr base path with request subpath", async () => {
      // addr has /headers, request has /sub → merged to /headers/sub
      // server matches startsWith("/headers") so this works
      const res = await proxyFetch(`http://127.0.0.1:${tcpPort}/headers`, `http://localhost/sub`);
      expect(res.status).toBe(200);
      const body = (await res.json()) as { url: string };
      expect(body.url).toBe("/headers/sub");
    });

    it("uses request path when addr has no path", async () => {
      const res = await proxyFetch(`http://127.0.0.1:${tcpPort}`, `http://localhost/json`);
      expect(res.status).toBe(200);
      expect(await res.json()).toEqual({ ok: true });
    });
  });

  describe("HTTPS upstream", () => {
    it("detects HTTPS from addr string", async () => {
      // We can't easily test real HTTPS without certs, but we can verify
      // that an https addr doesn't throw and properly rejects on connection
      // (which proves httpsRequest was selected, not httpRequest)
      await expect(proxyFetch(`https://127.0.0.1:1`, `http://localhost/json`)).rejects.toThrow();
    });

    it("uses HTTP by default for object addr", async () => {
      const res = await proxyFetch({ host: "127.0.0.1", port: tcpPort }, `http://localhost/json`);
      expect(res.status).toBe(200);
    });
  });
});


================================================
FILE: test/fixtures/agent2-cert.pem
================================================
-----BEGIN CERTIFICATE-----
MIIEIDCCAggCCQChRDh/XiBF+zANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJ1
czETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEeMBwGA1UE
AwwVRHVtbXkgSW50ZXJtZWRpYXRlIENBMB4XDTE4MDYyMjIwMzEwNFoXDTMyMDIy
OTIwMzEwNFowUDELMAkGA1UEBhMCdXMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAO
BgNVBAcMB1NlYXR0bGUxGjAYBgNVBAMMEWR1bW15LmV4YW1wbGUuY29tMIIBIjAN
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvSQq3d8AeZMTvtqZ13jWCckikyXJ
SACvkGCQUCJqOceESbg6IHdRzQdoccE4P3sbvNsf9BlbdJKM+neCxabqKaU1PPje
4P0tHT57t6yJrMuUh9NxEz3Bgh1srNHVS7saKvwHmcKm79jc+wxlioPmEQvQagjn
y7oTkyLt0sn4LGxBjrcv2JoHOC9f1pxX7l47MaiN0/ctRau7Nr3PFn+pkB4Yf6Z0
VyicVJbaUSz39Qo4HQWl1L2hiBP3CS1oKs2Yk0O1aOCMExWrhZQan+ZgHqL1rhgm
kPpw2/zwwPt5Vf9CSakvHwg198EXuTTXtkzYduuIJAm8yp969iEIiG2xTwIDAQAB
MA0GCSqGSIb3DQEBCwUAA4ICAQBnMSIo+kujkeXPh+iErFBmNtu/7EA+i/QnFPbN
lSLngclYYBJAGQI+DhirJI8ghDi6vmlHB2THewDaOJXEKvC1czE8064wioIcA9HJ
l3QJ3YYOFRctYdSHBU4TWdJbPgkLWDzYP5smjOfw8nDdr4WO/5jh9qRFcFpTFmQf
DyU3xgWLsQnNK3qXLdJjWG75pEhHR+7TGo+Ob/RUho/1RX/P89Ux7/oVbzdKqqFu
SErXAsjEIEFzWOM2uDOt6hrxDF6q+8/zudwQNEo422poEcTT9tDEFxMQ391CzZRi
nozBm4igRn1f5S3YZzLI6VEUns0s76BNy2CzvFWn40DziTqNBExAMfFFj76wiMsX
6fTIdcvkaTBa0S9SZB0vN99qahBdcG17rt4RssMHVRH1Wn7NXMwe476L0yXZ6gO7
Z4uNAPxgaI3BRP75EPfslLutCLZ+BC4Zzu6MY0Salbpfl0Go462EhsKCxvYhE2Dg
T477pICLfETZfA499Fd1tOaIsoLCrILAia/+Yd76uf94MuXUIqykDng/4H7xCc47
BZhNFJiPC6XHaXzN7NYSEUNX9VOwY8ncxKwtP6TXga96PdMUy/p98KIM8RZlDoxB
Xy9dcZBFNn/zrqjW7R0CCWCUriDIFSmEP0wDZ91YYa6BVuJMb5uL/USkTLpjZS4/
HNGvug==
-----END CERTIFICATE-----


================================================
FILE: test/fixtures/agent2-key.pem
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAvSQq3d8AeZMTvtqZ13jWCckikyXJSACvkGCQUCJqOceESbg6
IHdRzQdoccE4P3sbvNsf9BlbdJKM+neCxabqKaU1PPje4P0tHT57t6yJrMuUh9Nx
Ez3Bgh1srNHVS7saKvwHmcKm79jc+wxlioPmEQvQagjny7oTkyLt0sn4LGxBjrcv
2JoHOC9f1pxX7l47MaiN0/ctRau7Nr3PFn+pkB4Yf6Z0VyicVJbaUSz39Qo4HQWl
1L2hiBP3CS1oKs2Yk0O1aOCMExWrhZQan+ZgHqL1rhgmkPpw2/zwwPt5Vf9CSakv
Hwg198EXuTTXtkzYduuIJAm8yp969iEIiG2xTwIDAQABAoIBAGPIw/C/qJF7HYyv
6T+7GTiaa2o0IiehbP3/Y8NTFLWc49a8obXlHTvMr7Zr2I/tE+ojtIzkH9K1SjkN
eelqsNj9tsOPDI6oIvftsflpxkxqLtclnt8m0oMhoObf4OaONDT/N8dP4SBiSdsM
ZDmacnMFx5NZVWiup4sVf2CYexx7qks9FhyN2K5PArCQ4S9LHjFhSJVH4DSEpv7E
Ykbp30rhpqV7wSwjgUsm8ZYvI2NOlmffzLSiPdt3vy2K5Q25S/MVEAicg83rfDgK
6EluHjeygRI1xU6DJ0hU7tnU7zE9KURoHPUycO3BKzZnzUH26AA36I58Pu4fXWw/
Cgmbv2ECgYEA+og9E4ziKCEi3p8gqjIfwTRgWZxDLjEzooB/K0UhEearn/xiX29A
FiSzEHKfCB4uSrw5OENg2ckDs8uy08Qmxx7xFXL7AtufAl5fIYaWa0sNSqCaIk7p
ebbUmPcaYhKiLzIEd1EYEL38sXVZ62wvSVMRSWvEMq44g1qnoRlDa/8CgYEAwUTt
talYNwVmR9ZdkVEWm9ZxirdzoM6NaM6u4Tf34ygptpapdmIFSUhfq4iOiEnRGNg/
tuNqhNCIb3LNpJbhRPEzqN7E7qiF/mp7AcJgbuxLZBm12QuLuJdG3nrisKPFXcY1
lA4A7CFmNgH3E4THFfgwzyDXsBOxVLXleTqn+rECgYEA9up1P6J3dtOJuV2d5P/3
ugRz/X173LfTSxJXw36jZDAy8D/feG19/RT4gnplcKvGNhQiVOhbOOnbw0U8n2fQ
TCmbs+cZqyxnH/+AxNsPvvk+RVHZ93xMsY/XIldP4l65B8jFDA+Zp06IESI2mEeM
pzi+bd1Phh+dRSCA2865W2MCgYEAlxYsgmQ1WyX0dFpHYU+zzfXRYzDQyrhOYc2Z
duVK+yCto1iad7pfCY/zgmRJkI+sT7DV9kJIRjXDQuTLkEyHJF8vFGe6KhxCS8aw
DIsI2g4NTd6vg1J8UryoIUqNpqsQoqNNxUVBQVdG0ReuMGsPO8R/W50AIFz0txVP
o/rP0LECgYEA7e/mOwCnR+ovmS/CAksmos3oIqvkRkXNKpKe513FVmp3TpTU38ex
cBkFNU3hEO31FyrX1hGIKp3N5mHYSQ1lyODHM6teHW0OLWWTwIe8rIGvR2jfRLe0
bbkdj40atYVkfeFmpz9uHHG24CUYxJdPc360jbXTVp4i3q8zqgL5aMY=
-----END RSA PRIVATE KEY-----


================================================
FILE: test/http-proxy.test.ts
================================================
import { describe, it, expect } from "vitest";
import * as httpProxy from "../src/index.ts";
import http from "node:http";
import net from "node:net";
import * as ws from "ws";
import * as io from "socket.io";
import SSE from "sse";
import ioClient from "socket.io-client";
import type { AddressInfo } from "node:net";
import { listenOn, proxyListen } from "./_utils.ts";

// Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-http-proxy-test.js

describe("http-proxy", () => {
  describe("#createProxyServer", () => {
    it.skip("should throw without options", () => {
      let error;
      try {
        httpProxy.createProxyServer();
      } catch (error_) {
        error = error_;
      }

      expect(error).to.toBeInstanceOf(Error);
    });

    it("should return an object otherwise", () => {
      const obj = httpProxy.createProxyServer({
        target: "http://www.google.com:80",
      });

      expect(obj.web).to.toBeInstanceOf(Function);
      expect(obj.ws).to.instanceOf(Function);
      expect(obj.listen).to.instanceOf(Function);
    });
  });

  describe("#createProxyServer with forward options and using web-incoming passes", () => {
    it("should pipe the request using web-incoming#stream method", async () => {
      const source = http.createServer();
      const sourcePort = await listenOn(source);

      const proxy = httpProxy.createProxyServer({
        forward: "http://127.0.0.1:" + sourcePort,
      });
      const proxyPort = await proxyListen(proxy);

      const { promise, resolve } = Promise.withResolvers<void>();
      source.on("request", (req, res) => {
        expect(req.method).to.eql("GET");
        expect(Number.parseInt(req.headers.host!.split(":")[1]!)).toBe(proxyPort);
        source.close();
        proxy.close(resolve);
      });

      http.request("http://127.0.0.1:" + proxyPort, () => {}).end();

      await promise;
    });
  });

  describe("#createProxyServer using the web-incoming passes", () => {
    it("should proxy sse", async () => {
      const source = http.createServer();
      const sourcePort = await listenOn(source);

      const proxy = httpProxy.createProxyServer({
        target: "http://127.0.0.1:" + sourcePort,
      });
      const proxyPort = await proxyListen(proxy);

      const sse = new SSE(source, { path: "/" });
      sse.on("connection", (client) => {
        client.send("Hello over SSE");
        client.close();
      });

      const options = {
        hostname: "127.0.0.1",
        port: proxyPort,
      };

      const { promise, resolve } = Promise.withResolvers<void>();
      const req = http
        .request(options, (res) => {
          let streamData = "";
          res.on("data", (chunk) => {
            streamData += chunk.toString("utf8");
          });
          res.on("end", () => {
            expect(streamData).to.equal(":ok\n\ndata: Hello over SSE\n\n");
            source.close();
            proxy.close(resolve);
          });
        })
        .end();

      await promise;
    });

    it("should close downstream SSE stream when upstream aborts", async () => {
      const source = http.createServer((_, res) => {
        res.writeHead(200, {
          "content-type": "text/event-stream",
          "cache-control": "no-cache",
          connection: "keep-alive",
        });
        res.write(":ok\n\n");

        setTimeout(() => {
          res.socket?.destroy();
        }, 20);
      });
      const sourcePort = await listenOn(source);

      const proxy = httpProxy.createProxyServer({
        target: "http://127.0.0.1:" + sourcePort,
      });
      const proxyPort = await proxyListen(proxy);

      const { promise, resolve } = Promise.withResolvers<void>();
      let gotFirstChunk = false;
      let requestError: Error | undefined;

      const finish = () => {
        source.close();
        proxy.close(resolve);
      };

      const timeout = setTimeout(() => {
        requestError = new Error("Timed out waiting for downstream SSE close");
        finish();
      }, 1000);

      http
        .request(
          {
            hostname: "127.0.0.1",
            port: proxyPort,
            method: "GET",
          },
          (res) => {
            res.on("data", (chunk) => {
              if (chunk.toString("utf8").includes(":ok")) {
                gotFirstChunk = true;
              }
            });

            res.once("close", () => {
              clearTimeout(timeout);
              finish();
            });
          },
        )
        .on("error", (error) => {
          clearTimeout(timeout);
          requestError = error;
          finish();
        })
        .end();

      await promise;
      expect(requestError).toBeUndefined();
      expect(gotFirstChunk).toBe(true);
    });

    it("should destroy upstream proxy request when client aborts", async () => {
      const { promise, resolve, reject } = Promise.withResolvers<void>();

      // Track whether the upstream request was properly destroyed
      let upstreamReqDestroyed = false;

      const source = http.createServer((req, res) => {
        // SSE-like long-lived response
        res.writeHead(200, {
          "content-type": "text/event-stream",
          "cache-control": "no-cache",
          connection: "keep-alive",
        });
        res.write(":ok\n\n");

        req.socket.on("close", () => {
          upstreamReqDestroyed = true;
        });
      });
      const sourcePort = await listenOn(source);

      const proxy = httpProxy.createProxyServer({
        target: "http://127.0.0.1:" + sourcePort,
      });
      const proxyPort = await proxyListen(proxy);

      const timeout = setTimeout(() => {
        reject(new Error("Timed out: upstream request was not destroyed after client abort"));
      }, 2000);

      // Make a request and abort it after receiving the first chunk
      const clientReq = http.request(
        { hostname: "127.0.0.1", port: proxyPort, method: "GET" },
        (res) => {
          res.once("data", () => {
            // Client received data, now abort the connection
            clientReq.destroy();
          });
        },
      );
      clientReq.end();

      // Poll for upstream destruction
      const check = setInterval(() => {
        if (upstreamReqDestroyed) {
          clearInterval(check);
          clearTimeout(timeout);
          source.close();
          proxy.close(() => resolve());
        }
      }, 20);

      await promise;
      expect(upstreamReqDestroyed).toBe(true);
    });

    it("should make the request on pipe and finish it", async () => {
      const source = http.createServer();
      const sourcePort = await listenOn(source);

      const proxy = httpProxy.createProxyServer({
        target: "http://127.0.0.1:" + sourcePort,
      });
      const proxyPort = await proxyListen(proxy);

      const { promise, resolve } = Promise.withResolvers<void>();
      source.on("request", (req, res) => {
        expect(req.method).to.eql("POST");
        expect(req.headers["x-forwarded-for"]).to.eql("127.0.0.1");
        expect(Number.parseInt(req.headers.host!.split(":")[1]!)).to.eql(proxyPort);
        source.close();
        proxy.close(() => {});
        resolve();
      });

      http
        .request(
          {
            hostname: "127.0.0.1",
            port: proxyPort,
            method: "POST",
            headers: {
              "x-forwarded-for": "127.0.0.1",
            },
          },
          () => {},
        )
        .end();

      await promise;
    });
  });

  describe("#createProxyServer using the web-incoming passes", () => {
    it("should make the request, handle response and finish it", async () => {
      const source = http.createServer((req, res) => {
        expect(req.method).to.eql("GET");
        expect(Number.parseInt(req.headers.host!.split(":")[1]!)).to.eql(proxyPort);
        res.writeHead(200, { "Content-Type": "text/plain" });
        res.end("Hello from " + (source.address()! as any).port);
      });
      const sourcePort = await listenOn(source);

      const proxy = httpProxy.createProxyServer({
        target: "http://127.0.0.1:" + sourcePort,
        preserveHeaderKeyCase: true,
      });
      const proxyPort = await proxyListen(proxy);

      const { promise, resolve } = Promise.withResolvers<void>();
      http
        .request(
          {
            hostname: "127.0.0.1",
            port: proxyPort,
            method: "GET",
          },
          (res) => {
            expect(res.statusCode).to.eql(200);
            expect(res.headers["content-type"]).to.eql("text/plain");
            if (res.rawHeaders != undefined) {
              expect(res.rawHeaders.indexOf("Content-Type")).not.to.eql(-1);
              expect(res.rawHeaders.indexOf("text/plain")).not.to.eql(-1);
            }

            res.on("data", (data) => {
              expect(data.toString()).to.eql("Hello from " + sourcePort);
            });

            res.on("end", () => {
              source.close();
              proxy.close(resolve);
            });
          },
        )
        .end();
      await promise;
    });
  });

  describe("#createProxyServer() method with error response", () => {
    it("should make the request and emit the error event", async () => {
      const proxy = httpProxy.createProxyServer({
        target: "http://127.0.0.1:1",
      });

      const { promise, resolve } = Promise.withResolvers<void>();
      proxy.on("error", (err) => {
        expect(err).toBeInstanceOf(Error);
        expect((err as any).code).toBe("ECONNREFUSED");
        proxy.close(() => {});
        resolve();
      });

      const proxyPort = await proxyListen(proxy);

      http
        .request(
          {
            hostname: "127.0.0.1",
            port: proxyPort,
            method: "GET",
          },
          () => {},
        )
        .end();

      await promise.catch(() => {});
    });
  });

  describe("#createProxyServer setting the correct timeout value", () => {
    it("should hang up the socket at the timeout", async () => {
      const { promise, resolve } = Promise.withResolvers<void>();

      const source = http.createServer(function (_req, res) {
        setTimeout(() => {
          res.end("At this point the socket should be closed");
        }, 5);
      });
      const sourcePort = await listenOn(source);

      const proxy = httpProxy.createProxyServer({
        target: "http://127.0.0.1:" + sourcePort,
        timeout: 3,
      });
      const proxyPort = await proxyListen(proxy);

      proxy.on("error", (err) => {
        expect(err).toBeInstanceOf(Error);
        expect((err as any).code).toBe("ECONNRESET");
      });

      const testReq = http.request(
        {
          hostname: "127.0.0.1",
          port: proxyPort,
          method: "GET",
        },
        () => {},
      );

      testReq.on("error", (err) => {
        expect(err).toBeInstanceOf(Error);
        expect((err as any).code).toBe("ECONNRESET");
        proxy.close(() => {});
        source.close();
        resolve();
      });

      testReq.end();
      await promise;
    });
  });

  describe("#createProxyServer client disconnect", () => {
    it("should emit econnreset instead of error when client disconnects", async () => {
      const { promise, resolve, reject } = Promise.withResolvers<void>();

      // Target server that accepts connections but never responds
      const source = net.createServer();
      const sourcePort = await listenOn(source);

      const proxy = httpProxy.createProxyServer({
        target: "http://127.0.0.1:" + sourcePort,
      });

      // Intercept proxyReq to simulate the race condition where the client
      // has disconnected (socket is no longer writable) but req.socket.destroyed
      // is still false — reproducing the timing issue from upstream PR #1542.
      proxy.on("proxyReq", (proxyReq, req) => {
        setTimeout(() => {
          const socket = req.socket;
          Object.defineProperty(socket, "writable", { value: false, configurable: true });
          Object.defineProperty(socket, "destroyed", { value: false, configurable: true });
          proxyReq.emit(
            "error",
            Object.assign(new Error("read ECONNRESET"), { code: "ECONNRESET" }),
          );
        }, 50);
      });

      const proxyServer = http.createServer((req, res) => {
        proxy.web(req, res);
      });
      const proxyPort = await listenOn(proxyServer);

      proxy.on("econnreset", (err) => {
        expect(err).toBeInstanceOf(Error);
        expect((err as any).code).toBe("ECONNRESET");
        proxy.close(() => {});
        proxyServer.close();
        source.close();
        resolve();
      });

      proxy.on("error", (err) => {
        proxy.close(() => {});
        proxyServer.close();
        source.close();
        reject(new Error(`Unexpected error event: ${(err as any).code || err.message}`));
      });

      const testReq = http.request(
        {
          hostname: "127.0.0.1",
          port: proxyPort,
          method: "GET",
        },
        () => {},
      );

      testReq.on("error", () => {
        // Expected
      });

      testReq.end();

      await promise;
    });
  });

  describe("#createProxyServer with xfwd option", () => {
    it("should not throw on empty http host header", async () => {
      const source = http.createServer();
      const sourcePort = await listenOn(source);

      const proxy = httpProxy.createProxyServer({
        forward: "http://127.0.0.1:" + sourcePort,
        xfwd: true,
      });
      const proxyPort = await proxyListen(proxy);

      const { promise, resolve } = Promise.withResolvers<void>();
      source.on("request", function (req, _res) {
        expect(req.method).to.eql("GET");
        // Host header is forwarded from the original request (not changed to source)
        expect(req.headers["x-forwarded-for"]).toBeDefined();
        source.close();
        proxy.close(resolve);
      });

      const socket = net.connect({ port: proxyPort }, () => {
        socket.write("GET / HTTP/1.0\r\n\r\n");
      });

      socket.on("data", () => {
        socket.end();
      });

      // Ignore socket errors during teardown (server may close before socket drains)
      socket.on("error", () => {});

      http.request("http://127.0.0.1:" + proxyPort, () => {}).end();
      await promise;
    });
  });

  describe("#createProxyServer using the ws-incoming passes", () => {
    it("should proxy the websockets stream", async () => {
      const destiny = new ws.WebSocketServer({ port: 0 });
      await new Promise<void>((r) => destiny.on("listening", r));
      const sourcePort = (destiny.address() as AddressInfo).port;

      const proxy = httpProxy.createProxyServer({
        target: "ws://127.0.0.1:" + sourcePort,
        ws: true,
      });
      const proxyPort = await proxyListen(proxy);
      const proxyServer = proxy;

      const { promise, resolve } = Promise.withResolvers<void>();
      const client = new ws.WebSocket("ws://127.0.0.1:" + proxyPort);

      client.on("open", () => {
        client.send("hello there");
      });

      client.on("message", (msg) => {
        expect(msg.toString("utf8")).toBe("Hello over websockets");
        client.close();
        destiny.close();
        proxyServer.close(resolve);
      });

      destiny.on("connection", (socket) => {
        socket.on("message", (msg) => {
          expect(msg.toString("utf8")).toBe("hello there");
          socket.send("Hello over websockets");
        });
      });

      await promise;
    });

    it("should emit error on proxy error", async () => {
      const { promise, resolve } = Promise.withResolvers<void>();

      const proxy = httpProxy.createProxyServer({
        // Note: we don't ever listen on this port
        target: "ws://127.0.0.1:1",
        ws: true,
      });
      const proxyPort = await proxyListen(proxy);
      const proxyServer = proxy;
      const client 
Download .txt
gitextract_lci7uuca/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── autofix.yml
│       └── checks.yml
├── .gitignore
├── .oxfmtrc.json
├── .oxlintrc.json
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── LICENSE
├── README.md
├── bench/
│   ├── Dockerfile
│   ├── bench.ts
│   ├── package.json
│   ├── src/
│   │   ├── fast-proxy.ts
│   │   ├── fastify.ts
│   │   ├── http-proxy-3.ts
│   │   ├── http-proxy.ts
│   │   ├── httpxy-fetch.ts
│   │   ├── httpxy-server.ts
│   │   └── target.ts
│   └── test.ts
├── build.config.mjs
├── package.json
├── playground/
│   └── index.ts
├── pnpm-workspace.yaml
├── renovate.json
├── src/
│   ├── _utils.ts
│   ├── fetch.ts
│   ├── index.ts
│   ├── middleware/
│   │   ├── _utils.ts
│   │   ├── web-incoming.ts
│   │   ├── web-outgoing.ts
│   │   └── ws-incoming.ts
│   ├── server.ts
│   ├── types.ts
│   └── ws.ts
├── test/
│   ├── _stubs.ts
│   ├── _utils.test.ts
│   ├── _utils.ts
│   ├── fetch.test.ts
│   ├── fixtures/
│   │   ├── agent2-cert.pem
│   │   └── agent2-key.pem
│   ├── http-proxy.test.ts
│   ├── http2-proxy.test.ts
│   ├── https-proxy.test.ts
│   ├── index.test.ts
│   ├── middleware/
│   │   ├── web-incoming.test.ts
│   │   ├── web-outgoing.test.ts
│   │   └── ws-incoming.test.ts
│   ├── server.test.ts
│   ├── types.test-d.ts
│   ├── ws-destroyed-socket.test.ts
│   └── ws.test.ts
├── tsconfig.json
└── vitest.config.mjs
Download .txt
SYMBOL INDEX (142 symbols across 28 files)

FILE: bench/bench.ts
  constant IMAGE (line 19) | const IMAGE = "httpxy-bench";
  constant DURATION (line 20) | const DURATION = args.duration!;
  constant CONNECTIONS (line 21) | const CONNECTIONS = Number(args.connections);
  constant SEQUENTIAL (line 22) | const SEQUENTIAL = args.sequential!;
  constant POST_BODY (line 23) | const POST_BODY = JSON.stringify({
  constant TARGET_PORT (line 28) | const TARGET_PORT = 3000;
  constant PROXIES (line 30) | const PROXIES = [
  function cleanup (line 51) | function cleanup() {
  function dockerRun (line 60) | function dockerRun(...args: string[]) {
  function startContainer (line 66) | function startContainer(name: string, script: string, port: number) {
  function bombJson (line 86) | async function bombJson(args: string[]): Promise<string> {
  function waitForReady (line 109) | function waitForReady(port: number, retries = 60) {
  type BombardierResult (line 123) | interface BombardierResult {
  type BenchResult (line 148) | interface BenchResult {
  function formatNs (line 158) | function formatNs(ns: number): string {
  function formatThroughput (line 162) | function formatThroughput(bytesPerSec: number): string {
  function formatResult (line 168) | function formatResult(r: BenchResult): string {
  function parseResult (line 172) | function parseResult(json: string): BenchResult {
  constant HEADERS (line 195) | const HEADERS = ["Proxy", "Req/s", "Scale", "Avg", "P50", "P99", "Throug...
  function printTable (line 197) | function printTable(title: string, results: [name: string, result: Bench...
  function runBench (line 270) | async function runBench(label: string, extraArgs: string[] = []) {

FILE: bench/src/fast-proxy.ts
  constant PORT (line 4) | const PORT = Number(process.env.PORT) || 3003;
  constant TARGET (line 5) | const TARGET = process.env.TARGET || "http://target:3000";

FILE: bench/src/fastify.ts
  constant PORT (line 4) | const PORT = Number(process.env.PORT) || 3004;
  constant TARGET (line 5) | const TARGET = process.env.TARGET || "http://target:3000";

FILE: bench/src/http-proxy-3.ts
  constant PORT (line 4) | const PORT = Number(process.env.PORT) || 3005;
  constant TARGET (line 5) | const TARGET = process.env.TARGET || "http://target:3000";

FILE: bench/src/http-proxy.ts
  constant PORT (line 4) | const PORT = Number(process.env.PORT) || 3006;
  constant TARGET (line 5) | const TARGET = process.env.TARGET || "http://target:3000";

FILE: bench/src/httpxy-fetch.ts
  constant PORT (line 4) | const PORT = Number(process.env.PORT) || 3002;
  constant TARGET (line 5) | const TARGET = process.env.TARGET || "http://target:3000";
  function collectBody (line 7) | function collectBody(req: http.IncomingMessage): Promise<Buffer | undefi...

FILE: bench/src/httpxy-server.ts
  constant PORT (line 4) | const PORT = Number(process.env.PORT) || 3001;
  constant TARGET (line 5) | const TARGET = process.env.TARGET || "http://target:3000";

FILE: bench/src/target.ts
  constant PORT (line 3) | const PORT = Number(process.env.PORT) || 3000;

FILE: bench/test.ts
  constant TARGET_PORT (line 12) | const TARGET_PORT = 9_900;
  constant HTTPXY_SERVER_PORT (line 13) | const HTTPXY_SERVER_PORT = 9_901;
  constant HTTPXY_FETCH_PORT (line 14) | const HTTPXY_FETCH_PORT = 9_902;
  constant FAST_PROXY_PORT (line 15) | const FAST_PROXY_PORT = 9_903;
  constant FASTIFY_PROXY_PORT (line 16) | const FASTIFY_PROXY_PORT = 9_904;
  constant HTTP_PROXY_3_PORT (line 17) | const HTTP_PROXY_3_PORT = 9_905;
  constant HTTP_PROXY_PORT (line 18) | const HTTP_PROXY_PORT = 9_906;
  constant SMALL_BODY (line 20) | const SMALL_BODY = JSON.stringify({ message: "hello world", ts: Date.now...
  constant LARGE_BODY (line 21) | const LARGE_BODY = JSON.stringify({
  function createTargetServer (line 32) | function createTargetServer(): Promise<http.Server> {
  constant TARGET (line 56) | const TARGET = `http://127.0.0.1:${TARGET_PORT}`;
  function setupHttpxyServer (line 58) | async function setupHttpxyServer(): Promise<http.Server> {
  function collectBody (line 68) | function collectBody(req: http.IncomingMessage): Promise<Buffer | undefi...
  function setupHttpxyFetchServer (line 79) | async function setupHttpxyFetchServer(): Promise<http.Server> {
  function setupFastProxy (line 104) | async function setupFastProxy(): Promise<{ server: http.Server; close: (...
  function setupFastifyProxy (line 114) | async function setupFastifyProxy(): Promise<ReturnType<typeof Fastify>> {
  function setupHttpProxy3 (line 121) | async function setupHttpProxy3(): Promise<http.Server> {
  function setupHttpProxyLegacy (line 131) | async function setupHttpProxyLegacy(): Promise<http.Server> {
  type HttpResult (line 143) | interface HttpResult {
  function httpGet (line 149) | function httpGet(port: number, path = "/"): Promise<HttpResult> {
  function httpPost (line 166) | function httpPost(port: number, body: string, path = "/"): Promise<HttpR...
  function main (line 198) | async function main() {

FILE: playground/index.ts
  function main (line 4) | async function main() {

FILE: src/_utils.ts
  constant HTTP2_HEADER_BLACKLIST (line 29) | const HTTP2_HEADER_BLACKLIST = [":method", ":path", ":scheme", ":authori...
  function setupOutgoing (line 50) | function setupOutgoing(
  function joinURL (line 183) | function joinURL(base: string | undefined, path: string | undefined): st...
  function setupSocket (line 219) | function setupSocket(socket: net.Socket): net.Socket {
  function getPort (line 237) | function getPort(req: httpNative.IncomingMessage | Http2ServerRequest): ...
  function hasEncryptedConnection (line 255) | function hasEncryptedConnection(
  function rewriteCookieProperty (line 281) | function rewriteCookieProperty(
  function parseAddr (line 316) | function parseAddr(addr: string | ProxyAddr): ProxyAddr {
  function hasPort (line 340) | function hasPort(host: string | null | undefined): boolean {
  function requiresPort (line 353) | function requiresPort(_port: string | number, _protocol: string | undefi...

FILE: src/fetch.ts
  type ProxyFetchOptions (line 11) | interface ProxyFetchOptions {
  function proxyFetch (line 58) | async function proxyFetch(
  function toInit (line 211) | function toInit(init?: RequestInit | Request): RequestInit | undefined {
  function _toNodeStream (line 227) | function _toNodeStream(body: BodyInit | null | undefined): Readable | Bu...
  function _bufferBody (line 247) | async function _bufferBody(body: BodyInit | null | undefined): Promise<B...
  type _RequestOpts (line 273) | interface _RequestOpts {
  function _sendRequest (line 283) | function _sendRequest(

FILE: src/middleware/_utils.ts
  type ResOfType (line 7) | type ResOfType<T extends "web" | "ws"> = T extends "ws"
  type ProxyMiddleware (line 15) | type ProxyMiddleware<T extends ServerResponse | Http2ServerResponse | So...
  function defineProxyMiddleware (line 27) | function defineProxyMiddleware<T extends ServerResponse | Socket = Serve...
  type ProxyOutgoingMiddleware (line 33) | type ProxyOutgoingMiddleware = (
  function defineProxyOutgoingMiddleware (line 43) | function defineProxyOutgoingMiddleware(m: ProxyOutgoingMiddleware) {

FILE: src/middleware/web-incoming.ts
  function createErrorHandler (line 130) | function createErrorHandler(proxyReq: ClientRequest, url: URL | ProxyTar...
  function handleResponse (line 171) | function handleResponse(proxyRes: IncomingMessage, redirectCount: number...

FILE: src/middleware/web-outgoing.ts
  function _toURL (line 163) | function _toURL(target: ProxyTarget): URL {

FILE: src/middleware/ws-incoming.ts
  function onSocketError (line 158) | function onSocketError(err: Error) {
  function onOutgoingError (line 167) | function onOutgoingError(err: Error) {

FILE: src/server.ts
  type ProxyServerEventMap (line 11) | interface ProxyServerEventMap<
  class ProxyServer (line 35) | class ProxyServer<
    method constructor (line 55) | constructor(options: ProxyServerOptions = {}) {
    method listen (line 71) | listen(port: number, hostname?: string, listeningListener?: () => void) {
    method close (line 104) | close(callback?: () => void) {
    method before (line 116) | before<Type extends "ws" | "web">(
    method after (line 137) | after<Type extends "ws" | "web">(
    method _getPasses (line 159) | _getPasses<Type extends "ws" | "web">(type: Type): ProxyMiddleware<Res...
  function createProxyServer (line 180) | function createProxyServer(options: ProxyServerOptions = {}) {
  function _createProxyFn (line 186) | function _createProxyFn<

FILE: src/types.ts
  type ProxyTargetDetailed (line 3) | interface ProxyTargetDetailed {
  type ProxyTarget (line 18) | type ProxyTarget = string | URL | ProxyTargetDetailed;
  type ProxyAddr (line 21) | type ProxyAddr =
  type ProxyServerOptions (line 25) | interface ProxyServerOptions {

FILE: src/ws.ts
  type ProxyUpgradeOptions (line 19) | interface ProxyUpgradeOptions {
  function proxyUpgrade (line 90) | function proxyUpgrade(
  function _buildTargetURL (line 222) | function _buildTargetURL(addr: ProxyAddr, useSSL = false): URL {
  function _createHttpHeader (line 232) | function _createHttpHeader(

FILE: test/_stubs.ts
  type OutgoingOptions (line 14) | type OutgoingOptions = Omit<RequestOptions & HttpsRequestOptions, "heade...
  function createOutgoing (line 18) | function createOutgoing(): OutgoingOptions {
  function stubIncomingMessage (line 24) | function stubIncomingMessage(overrides: Record<string, unknown> = {}): I...
  function stubServerResponse (line 37) | function stubServerResponse(overrides: Record<string, unknown> = {}): Se...
  function stubSocket (line 43) | function stubSocket(overrides: Record<string, unknown> = {}): Socket {
  type MiddlewareOptions (line 49) | type MiddlewareOptions = ProxyServerOptions & {
  function stubMiddlewareOptions (line 54) | function stubMiddlewareOptions(overrides: Record<string, unknown> = {}):...
  function stubProxyServer (line 60) | function stubProxyServer(overrides: Record<string, unknown> = {}): Proxy...

FILE: test/_utils.ts
  function listenOn (line 7) | function listenOn(server: http.Server | https.Server | net.Server): Prom...
  function proxyListen (line 16) | function proxyListen(

FILE: test/fetch.test.ts
  method start (line 203) | start(controller) {
  method start (line 246) | start(controller) {
  method start (line 280) | start(controller) {
  method start (line 320) | start(controller) {

FILE: test/http-proxy.test.ts
  function maybe_done (line 539) | function maybe_done() {
  function startSocketIo (line 637) | function startSocketIo() {
  function startSocketIo (line 677) | function startSocketIo() {

FILE: test/index.test.ts
  type Listener (line 7) | type Listener = {
  function listen (line 12) | function listen(handler: (req: IncomingMessage, res: ServerResponse) => ...

FILE: test/middleware/web-incoming.test.ts
  function requestHandler (line 272) | function requestHandler(req: any, res: any) {
  function requestHandler (line 303) | function requestHandler(req: any, res: any) {
  function requestHandler (line 333) | function requestHandler(req: any, res: any) {
  function requestHandler (line 369) | async function requestHandler(req: any, res: any) {
  function requestHandler (line 400) | function requestHandler(req: any, res: any) {
  function requestHandler (line 427) | function requestHandler(req: any, res: any) {
  function requestHandler (line 462) | function requestHandler(req: any, res: any) {
  function requestHandler (line 498) | function requestHandler(req: any, res: any) {
  function requestHandler (line 530) | function requestHandler(req: any, res: any) {
  function requestHandler (line 578) | function requestHandler(req: any, res: any) {
  function requestHandler (line 607) | function requestHandler(req: any, res: any) {
  function requestHandler (line 642) | function requestHandler(req: any, res: any) {

FILE: test/middleware/web-outgoing.test.ts
  method setHeader (line 652) | setHeader(k: string, v: string | string[]) {
  method setHeader (line 682) | setHeader(k: string, v: string | string[]) {

FILE: test/ws-destroyed-socket.test.ts
  function listenOn (line 18) | function listenOn(server: http.Server | net.Server): Promise<number> {

FILE: test/ws.test.ts
  function createProxyServer (line 42) | function createProxyServer(addr: string | ProxyAddr, opts?: Parameters<t...
  function listenServer (line 55) | async function listenServer(server: Server): Promise<number> {
  function makeDummySocket (line 62) | function makeDummySocket() {
  function wsUpgradeRequest (line 71) | function wsUpgradeRequest(port: number): string {
  function createTargetServer (line 83) | async function createTargetServer(
Condensed preview — 56 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (351K chars).
[
  {
    "path": ".editorconfig",
    "chars": 224,
    "preview": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\ncharset = utf-8\n\n[*.js]\nin"
  },
  {
    "path": ".github/workflows/autofix.yml",
    "chars": 501,
    "preview": "name: autofix.ci\non: { push: {}, pull_request: {} }\npermissions: { contents: read }\njobs:\n  autofix:\n    runs-on: ubuntu"
  },
  {
    "path": ".github/workflows/checks.yml",
    "chars": 489,
    "preview": "name: checks\non: { push: {}, pull_request: {} }\njobs:\n  checks:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: acti"
  },
  {
    "path": ".gitignore",
    "chars": 78,
    "preview": "node_modules\ncoverage\ndist\ntypes\n.vscode\n.DS_Store\n.eslintcache\n*.log*\n*.env*\n"
  },
  {
    "path": ".oxfmtrc.json",
    "chars": 69,
    "preview": "{\n  \"$schema\": \"https://unpkg.com/oxfmt/configuration_schema.json\"\n}\n"
  },
  {
    "path": ".oxlintrc.json",
    "chars": 163,
    "preview": "{\n  \"$schema\": \"https://unpkg.com/oxlint/configuration_schema.json\",\n  \"plugins\": [\"unicorn\", \"typescript\", \"oxc\"],\n  \"r"
  },
  {
    "path": "AGENTS.md",
    "chars": 12850,
    "preview": "# httpxy — Agent Guide\n\nFull-featured HTTP/WebSocket proxy for Node.js. Zero production dependencies. Originally forked "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 12801,
    "preview": "# Changelog\n\n## v0.5.1\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.5.0...v0.5.1)\n\n### 🚀 Enhancements\n\n-"
  },
  {
    "path": "CLAUDE.md",
    "chars": 11,
    "preview": "@AGENTS.md\n"
  },
  {
    "path": "LICENSE",
    "chars": 2232,
    "preview": "MIT License\n\nCopyright (c) Pooya Parsa <pooya@pi0.io>\n\nPermission is hereby granted, free of charge, to any person obtai"
  },
  {
    "path": "README.md",
    "chars": 11782,
    "preview": "# 🔀 httpxy\n\n[![npm version][npm-version-src]][npm-version-href]\n[![npm downloads][npm-downloads-src]][npm-downloads-href"
  },
  {
    "path": "bench/Dockerfile",
    "chars": 347,
    "preview": "FROM node:lts\n\nCOPY --from=alpine/bombardier /usr/bin/bombardier /usr/local/bin/bombardier\nRUN corepack enable && corepa"
  },
  {
    "path": "bench/bench.ts",
    "chars": 8656,
    "preview": "#!/usr/bin/env node\n\nimport { execSync, execFileSync, execFile as _execFile } from \"node:child_process\";\nimport { parseA"
  },
  {
    "path": "bench/package.json",
    "chars": 301,
    "preview": "{\n  \"name\": \"bench\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"devDependencies\": {\n    \"@fastify/http-proxy\": \"^11.4.2\","
  },
  {
    "path": "bench/src/fast-proxy.ts",
    "chars": 415,
    "preview": "import http from \"node:http\";\nimport fastProxy from \"fast-proxy\";\n\nconst PORT = Number(process.env.PORT) || 3003;\nconst "
  },
  {
    "path": "bench/src/fastify.ts",
    "chars": 382,
    "preview": "import Fastify from \"fastify\";\nimport httpProxy from \"@fastify/http-proxy\";\n\nconst PORT = Number(process.env.PORT) || 30"
  },
  {
    "path": "bench/src/http-proxy-3.ts",
    "chars": 427,
    "preview": "import http from \"node:http\";\nimport { createProxyServer } from \"http-proxy-3\";\n\nconst PORT = Number(process.env.PORT) |"
  },
  {
    "path": "bench/src/http-proxy.ts",
    "chars": 421,
    "preview": "import http from \"node:http\";\nimport httpProxy from \"http-proxy\";\n\nconst PORT = Number(process.env.PORT) || 3006;\nconst "
  },
  {
    "path": "bench/src/httpxy-fetch.ts",
    "chars": 1177,
    "preview": "import http from \"node:http\";\nimport { proxyFetch } from \"../../src/index.ts\";\n\nconst PORT = Number(process.env.PORT) ||"
  },
  {
    "path": "bench/src/httpxy-server.ts",
    "chars": 440,
    "preview": "import http from \"node:http\";\nimport { createProxyServer } from \"../../src/index.ts\";\n\nconst PORT = Number(process.env.P"
  },
  {
    "path": "bench/src/target.ts",
    "chars": 681,
    "preview": "import http from \"node:http\";\n\nconst PORT = Number(process.env.PORT) || 3000;\n\nconst server = http.createServer((req, re"
  },
  {
    "path": "bench/test.ts",
    "chars": 8286,
    "preview": "#!/usr/bin/env node\nimport http from \"node:http\";\nimport { createProxyServer, proxyFetch } from \"../src/index.ts\";\nimpor"
  },
  {
    "path": "build.config.mjs",
    "chars": 177,
    "preview": "import { defineBuildConfig } from \"obuild/config\";\n\nexport default defineBuildConfig({\n  entries: [\n    {\n      type: \"b"
  },
  {
    "path": "package.json",
    "chars": 1519,
    "preview": "{\n  \"name\": \"httpxy\",\n  \"version\": \"0.5.1\",\n  \"description\": \"A full-featured HTTP proxy for Node.js.\",\n  \"license\": \"MI"
  },
  {
    "path": "playground/index.ts",
    "chars": 996,
    "preview": "import http from \"node:http\";\nimport { createProxyServer } from \"../src/index.ts\";\n\nasync function main() {\n  const main"
  },
  {
    "path": "pnpm-workspace.yaml",
    "chars": 20,
    "preview": "packages:\n  - bench\n"
  },
  {
    "path": "renovate.json",
    "chars": 49,
    "preview": "{\n  \"extends\": [\"github>unjs/renovate-config\"]\n}\n"
  },
  {
    "path": "src/_utils.ts",
    "chars": 10850,
    "preview": "import httpNative from \"node:http\";\nimport httpsNative from \"node:https\";\nimport net from \"node:net\";\nimport type { Prox"
  },
  {
    "path": "src/fetch.ts",
    "chars": 11530,
    "preview": "import type { IncomingMessage, RequestOptions } from \"node:http\";\nimport { request as httpRequest } from \"node:http\";\nim"
  },
  {
    "path": "src/index.ts",
    "chars": 318,
    "preview": "export type { ProxyAddr, ProxyServerOptions, ProxyTarget, ProxyTargetDetailed } from \"./types.ts\";\nexport { ProxyServer,"
  },
  {
    "path": "src/middleware/_utils.ts",
    "chars": 1498,
    "preview": "import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { Socket } from \"node:net\";\nimport type { "
  },
  {
    "path": "src/middleware/web-incoming.ts",
    "chars": 9470,
    "preview": "import type { ClientRequest, IncomingMessage, ServerResponse } from \"node:http\";\nimport type { ProxyTargetDetailed } fro"
  },
  {
    "path": "src/middleware/web-outgoing.ts",
    "chars": 5881,
    "preview": "import { rewriteCookieProperty } from \"../_utils.ts\";\nimport type { ProxyTarget, ProxyTargetDetailed } from \"../types.ts"
  },
  {
    "path": "src/middleware/ws-incoming.ts",
    "chars": 5567,
    "preview": "import nodeHTTP from \"node:http\";\nimport nodeHTTPS from \"node:https\";\nimport type { Socket } from \"node:net\";\nimport { t"
  },
  {
    "path": "src/server.ts",
    "chars": 8299,
    "preview": "import http from \"node:http\";\nimport https from \"node:https\";\nimport http2 from \"node:http2\";\nimport { EventEmitter } fr"
  },
  {
    "path": "src/types.ts",
    "chars": 3272,
    "preview": "import type * as stream from \"node:stream\";\n\nexport interface ProxyTargetDetailed {\n  host?: string;\n  port?: number | s"
  },
  {
    "path": "src/ws.ts",
    "chars": 7299,
    "preview": "import type { IncomingMessage, RequestOptions } from \"node:http\";\nimport { request as httpRequest } from \"node:http\";\nim"
  },
  {
    "path": "test/_stubs.ts",
    "chars": 1697,
    "preview": "import type {\n  IncomingMessage,\n  OutgoingHttpHeaders,\n  RequestOptions,\n  ServerResponse,\n} from \"node:http\";\nimport t"
  },
  {
    "path": "test/_utils.test.ts",
    "chars": 22147,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport * as common from \"../src/_utils.ts\";\nimport { createOutgoing, stub"
  },
  {
    "path": "test/_utils.ts",
    "chars": 851,
    "preview": "import http from \"node:http\";\nimport https from \"node:https\";\nimport net from \"node:net\";\nimport type { AddressInfo } fr"
  },
  {
    "path": "test/fetch.test.ts",
    "chars": 21113,
    "preview": "import { createServer, type Server } from \"node:http\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path"
  },
  {
    "path": "test/fixtures/agent2-cert.pem",
    "chars": 1493,
    "preview": "-----BEGIN CERTIFICATE-----\nMIIEIDCCAggCCQChRDh/XiBF+zANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJ1\nczETMBEGA1UECAwKV2FzaGluZ3R"
  },
  {
    "path": "test/fixtures/agent2-key.pem",
    "chars": 1679,
    "preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEAvSQq3d8AeZMTvtqZ13jWCckikyXJSACvkGCQUCJqOceESbg6\nIHdRzQdoccE4P3sbvNsf9Bl"
  },
  {
    "path": "test/http-proxy.test.ts",
    "chars": 27662,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport * as httpProxy from \"../src/index.ts\";\nimport http from \"node:http"
  },
  {
    "path": "test/http2-proxy.test.ts",
    "chars": 5106,
    "preview": "import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as httpProxy from \"../src/index.ts\";\nim"
  },
  {
    "path": "test/https-proxy.test.ts",
    "chars": 8725,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport * as httpProxy from \"../src/index.ts\";\nimport http from \"node:http"
  },
  {
    "path": "test/index.test.ts",
    "chars": 7399,
    "preview": "import { afterAll, describe, expect, it } from \"vitest\";\nimport http, { type IncomingMessage, type ServerResponse } from"
  },
  {
    "path": "test/middleware/web-incoming.test.ts",
    "chars": 32072,
    "preview": "import { describe, it, expect } from \"vitest\";\n\nimport * as webPasses from \"../../src/middleware/web-incoming.ts\";\nimpor"
  },
  {
    "path": "test/middleware/web-outgoing.test.ts",
    "chars": 24768,
    "preview": "import { describe, it, expect, beforeEach } from \"vitest\";\n\nimport * as webOutgoing from \"../../src/middleware/web-outgo"
  },
  {
    "path": "test/middleware/ws-incoming.test.ts",
    "chars": 4898,
    "preview": "import { describe, it, expect } from \"vitest\";\nimport * as wsIncoming from \"../../src/middleware/ws-incoming.ts\";\nimport"
  },
  {
    "path": "test/server.test.ts",
    "chars": 9621,
    "preview": "import { describe, it, expect, afterEach, vi } from \"vitest\";\nimport http from \"node:http\";\nimport type { AddressInfo } "
  },
  {
    "path": "test/types.test-d.ts",
    "chars": 1351,
    "preview": "import { assertType, describe, expectTypeOf, it } from \"vitest\";\nimport { ProxyServer } from \"../src/server.ts\";\nimport "
  },
  {
    "path": "test/ws-destroyed-socket.test.ts",
    "chars": 4094,
    "preview": "import { describe, it } from \"vitest\";\nimport http from \"node:http\";\nimport net from \"node:net\";\nimport type { AddressIn"
  },
  {
    "path": "test/ws.test.ts",
    "chars": 27286,
    "preview": "import { createServer, type Server, type IncomingMessage } from \"node:http\";\nimport { createServer as createHTTPSServer "
  },
  {
    "path": "tsconfig.json",
    "chars": 556,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"module\": \"nodenext\",\n    \"moduleResolution\": \"nodenext\",\n    \"modu"
  },
  {
    "path": "vitest.config.mjs",
    "chars": 185,
    "preview": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    typecheck: { enabled: true },"
  }
]

About this extraction

This page contains the full source code of the unjs/httpxy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 56 files (324.4 KB), approximately 84.5k tokens, and a symbol index with 142 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!