[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\ncharset = utf-8\n\n[*.js]\nindent_style = space\nindent_size = 2\n\n[{package.json,*.yml,*.cjson}]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".github/workflows/autofix.yml",
    "content": "name: autofix.ci\non: { push: {}, pull_request: {} }\npermissions: { contents: read }\njobs:\n  autofix:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - run: npm i -fg corepack && corepack enable\n      - uses: actions/setup-node@v6\n        with: { node-version: lts/*, cache: \"pnpm\" }\n      - run: pnpm install\n      - run: pnpm fmt\n      - uses: autofix-ci/action@7a166d7532b277f34e16238930461bf77f9d7ed8\n        with: { commit-message: \"chore: apply automated updates\" }\n"
  },
  {
    "path": ".github/workflows/checks.yml",
    "content": "name: checks\non: { push: {}, pull_request: {} }\njobs:\n  checks:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - run: npm i -fg corepack && corepack enable\n      - uses: actions/setup-node@v6\n        with: { node-version: lts/*, cache: \"pnpm\" }\n      - run: pnpm install\n      - run: pnpm typecheck\n      - run: pnpm vitest --coverage\n      - run: pnpm run lint\n      - uses: codecov/codecov-action@v6\n        with: { token: \"${{ secrets.CODECOV_TOKEN }}\" }\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\ncoverage\ndist\ntypes\n.vscode\n.DS_Store\n.eslintcache\n*.log*\n*.env*\n"
  },
  {
    "path": ".oxfmtrc.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/oxfmt/configuration_schema.json\"\n}\n"
  },
  {
    "path": ".oxlintrc.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/oxlint/configuration_schema.json\",\n  \"plugins\": [\"unicorn\", \"typescript\", \"oxc\"],\n  \"rules\": {\n    \"no-unused-vars\": \"off\"\n  }\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# httpxy — Agent Guide\n\nFull-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).\n\n## API\n\n- **`createProxyServer(opts)`** / **`ProxyServer`** — Traditional event-driven proxy server with middleware pipeline\n- **`proxyFetch(addr, input, init?)`** — Web-standard `Request`/`Response` interface for proxying individual requests\n- **`proxyUpgrade(addr, req, socket, head?, opts?)`** — Standalone WebSocket upgrade proxy without a `ProxyServer` instance\n\n## Source Architecture (`src/`)\n\n```\nsrc/\n├── index.ts              — Re-exports (entry point)\n├── types.ts              — ProxyTarget, ProxyServerOptions, ProxyTargetDetailed\n├── server.ts             — ProxyServer class (EventEmitter), createProxyServer()\n├── fetch.ts              — proxyFetch() using Node.js http module → Web Response\n├── upgrade.ts            — proxyUpgrade() standalone WebSocket upgrade proxy\n├── _utils.ts             — setupOutgoing(), setupSocket(), joinURL(), cookie/header helpers\n└── middleware/\n    ├── _utils.ts          — Middleware type definitions (ProxyMiddleware, ProxyOutgoingMiddleware)\n    ├── web-incoming.ts    — HTTP request passes: deleteLength → timeout → XHeaders → stream\n    ├── web-outgoing.ts    — HTTP response passes: removeChunked → setConnection → setRedirectHostRewrite → writeHeaders → writeStatusCode\n    └── ws-incoming.ts     — WebSocket passes: checkMethodAndHeader → XHeaders → stream\n```\n\n### Request flow (HTTP)\n\n```\nClient → ProxyServer.web() → web-incoming passes → http.request(target) → target server\nTarget response → web-outgoing passes → client response\n```\n\n### Request flow (WebSocket)\n\n```\nClient upgrade → ProxyServer.ws() → ws-incoming passes → http.request(target)\nTarget upgrade → bidirectional socket pipe\n```\n\n### Request flow (proxyFetch)\n\n```\nproxyFetch(addr, request) → http.request to addr → Web Response\n```\n\n### Request flow (proxyUpgrade)\n\n```\nproxyUpgrade(addr, req, socket, head) → http.request to addr → upgrade → bidirectional socket pipe\nReturns Promise<Socket> (the upstream proxy socket)\n```\n\n### Key design patterns\n\n- **Middleware pipeline**: Passes are functions that run in order; returning `true` halts the chain\n- **Event-driven**: `ProxyServer` emits lifecycle events (`start`, `proxyReq`, `proxyRes`, `end`, `error`, `open`, `close`)\n- **Extensible middleware**: `server.before(type, passName, fn)` / `server.after(type, passName, fn)` to insert custom passes\n- **Flexible targets**: TCP (`host:port`), Unix socket (`socketPath`), or URL string\n\n## Behavioral Notes (Source + Tests)\n\n- `proxy.web()` / `proxy.ws()` return a Promise and can either reject (no `error` listener, request/response error) or resolve after `res.close`.\n- Per-call options are merged as `{ ...opts, ...server.options }`, so constructor options override per-call options on key conflicts.\n- String `target`/`forward` values are normalized to `URL` objects before middleware execution.\n- Missing both `target` and `forward` emits `error` with message `\"Must provide a proper URL as target\"`.\n- Middleware names are often empty strings (passes are wrapped arrow functions). Tests use `before(\"web\", \"\", ...)` / `after(\"web\", \"\", ...)`.\n- `ProxyServer.close()` is a no-op before `listen()`, and sets internal `_server` to `undefined` after close callback.\n\n### HTTP middleware semantics\n\n- Incoming pass order is fixed: `deleteLength -> timeout -> XHeaders -> stream`.\n- `deleteLength` applies to both `DELETE` and `OPTIONS` without content length; it sets `content-length: 0` and removes `transfer-encoding`.\n- `proxyReq` event is intentionally skipped when request has `expect` header (`100-continue` advisory coverage).\n- `selfHandleResponse: true` skips outgoing passes and auto-pipe; callers must finish the response in `proxyRes`.\n- `proxyTimeout` aborts upstream request and surfaces timeout errors (tested as `ECONNRESET`).\n- `followRedirects: true | number` enables native redirect following (301/302/303/307/308). `true` = max 5 hops, number = custom max.\n- On 301/302/303 redirects, method changes to GET and request body is dropped.\n- On 307/308 redirects, original method and body are preserved (body is buffered on first request for replay).\n- `proxyRes` event fires only for the final (non-redirect) response; `proxyReq` fires for each request including redirects.\n- Sensitive headers (`authorization`, `cookie`) are stripped on cross-origin redirects.\n- When `followRedirects` is enabled, the request body is tee'd (written to proxy request and buffered simultaneously) rather than piped.\n\n### WebSocket middleware semantics\n\n- Incoming pass order is fixed: `checkMethodAndHeader -> XHeaders -> stream`.\n- WS requests must be `GET` with `upgrade: websocket`; otherwise socket is destroyed and the chain stops.\n- `proxyReqWs`, `open`, `close`, and deprecated `proxySocket` events are part of tested flow.\n- Upgrade response headers preserve repeated headers like multiple `Set-Cookie` values.\n\n### Outgoing response semantics\n\n- Outgoing pass order is fixed: `removeChunked -> setConnection -> setRedirectHostRewrite -> writeHeaders -> writeStatusCode`.\n- Redirect rewrite applies on `201`, `301`, `302`, `303`, `307`, `308` only, and only when `Location` host matches `target.host`.\n- `hostRewrite` takes precedence over `autoRewrite`; `protocolRewrite` composes with either.\n- Cookie rewriting supports string or mapping config (including wildcard `\"*\"` and empty string for removal).\n- `preserveHeaderKeyCase` uses `rawHeaders` when available.\n\n### URL/path handling invariants\n\n- Path joining does not normalize repeated slashes (`/a/b//c` stays as-is).\n- `joinURL()` always returns a usable path and avoids duplicate slash insertion.\n- `toProxy: true` preserves absolute URL in outgoing path as `\"/\" + req.url`.\n- `ignorePath` drops request path; with `prependPath: false` the outgoing path becomes `/`.\n\n### `proxyFetch` semantics\n\n- `addr` accepts `http://host:port`, `https://host:port`, `unix:/path.sock`, or object form `{ host, port }` / `{ socketPath }`.\n- Both HTTP and HTTPS upstream targets are supported. HTTPS is auto-detected from the `addr` string protocol.\n- 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()`.\n- Redirect mode defaults to `manual`.\n- Streaming request bodies are supported (`ReadableStream`) and set `duplex: \"half\"`.\n- Hop-by-hop response headers `transfer-encoding`, `keep-alive`, `connection` are stripped.\n- Response body is `null` for `204` and `304`.\n- Network and request-body-stream errors reject the Promise.\n- Accepts optional `ProxyFetchOptions` as 4th argument with `timeout`, `xfwd`, `changeOrigin`, `agent`, `followRedirects`, and `ssl`.\n- `timeout` sets a deadline on the upstream request; rejects with `\"Proxy request timed out\"` on expiry.\n- `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.\n- `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).\n- `agent` enables connection pooling/reuse via a custom `http.Agent`. Defaults to `false` (no agent).\n- `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.\n- `ssl` passes TLS options to `https.request` (e.g. `{ rejectUnauthorized: false }`).\n- `AbortSignal` support is wired through `init.signal` (standard `RequestInit`), aborting the underlying `http.request`.\n- Multi-value request headers are preserved as arrays (not flattened by the `Headers` API).\n- Body types `ArrayBuffer`, `TypedArray`, and `Blob` are properly converted to `Buffer` before sending.\n\n### `proxyUpgrade` semantics\n\n- Standalone WebSocket upgrade proxy — no `ProxyServer` instance or `EventEmitter` needed.\n- `addr` accepts same formats as `proxyFetch`: `http://host:port`, `ws://host:port`, `unix:/path`, or object `{ host, port }` / `{ socketPath }`.\n- Validates that the request is a valid WS upgrade (`GET` + `upgrade: websocket`); rejects with error and destroys socket otherwise.\n- `xfwd` is enabled by default (unlike `ProxyServer` where it defaults to `false`). Pass `xfwd: false` to disable.\n- Supports `xfwd`, `changeOrigin`, `headers`, `ssl`, `secure`, `agent`, `auth`, `prependPath`, `ignorePath`, `toProxy` options via `ProxyUpgradeOptions`.\n- Returns `Promise<Socket>` — resolves with the upstream proxy socket on successful upgrade, rejects on connection or socket error.\n- If the upstream responds without upgrading (e.g., 404), the response is relayed to the client socket.\n- Uses `setupOutgoing()` and `setupSocket()` from shared utils, consistent with `ProxyServer.ws()`.\n\n## Tests (`test/`)\n\n```\ntest/\n├── index.test.ts                  — Main proxy: paths, headers, changeOrigin, xfwd, WebSocket, errors\n├── fetch.test.ts                  — proxyFetch: TCP/Unix, GET/POST, redirects, cookies, 204/304, signal, timeout, xfwd, changeOrigin\n├── upgrade.test.ts                — proxyUpgrade: WS proxy, addr formats, xfwd, error handling\n├── http-proxy.test.ts             — Forward, target, WebSocket, socket.io, SSE, timeouts, error events\n├── https-proxy.test.ts            — HTTPS targets, SSL certs, certificate validation\n├── _utils.test.ts                 — setupOutgoing, setupSocket, path joining, auth, changeOrigin\n├── types.test-d.ts                — TypeScript type assertions (vitest typecheck)\n└── middleware/\n    ├── web-incoming.test.ts       — deleteLength, timeout, XHeaders\n    ├── web-outgoing.test.ts       — Redirect rewrite, writeHeaders, cookies, status codes\n    └── ws-incoming.test.ts        — Method/header validation, error handling\n```\n\n### Running tests\n\n```bash\npnpm vitest run test/<file>       # Single test file\npnpm vitest run                   # All tests\npnpm test                         # Lint + typecheck + tests with coverage\n```\n\n### Test expectations and parity\n\n- 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`).\n- `followRedirects` is natively implemented (no external dependency). See behavioral notes below.\n- HTTPS tests rely on local fixtures in `test/fixtures/agent2-*.pem`.\n\n## Tooling\n\n| Tool      | Command          | Notes                                         |\n| --------- | ---------------- | --------------------------------------------- |\n| Build     | `pnpm build`     | Uses `unbuild` → CJS + ESM + types in `dist/` |\n| Dev       | `pnpm dev`       | Vitest watch mode                             |\n| Lint      | `pnpm lint`      | `oxlint` + `oxfmt --check`                    |\n| Format    | `pnpm fmt`       | `oxlint --fix` + `oxfmt`                      |\n| Typecheck | `pnpm typecheck` | `tsgo --noEmit` (native TS preview)           |\n| Test      | `pnpm test`      | Full: lint + typecheck + vitest with coverage |\n\n## Key Types\n\n```ts\ntype ProxyTarget = string | URL | ProxyTargetDetailed;\n\ninterface ProxyTargetDetailed {\n  host?: string;\n  port?: number | string;\n  protocol?: string;\n  hostname?: string;\n  socketPath?: string;\n  // TLS: key, passphrase, pfx, cert, ca, ciphers, secureProtocol\n}\n\ninterface ProxyServerOptions {\n  target?: ProxyTarget; // Proxy destination\n  forward?: ProxyTarget; // Forward destination\n  ws?: boolean; // Enable WebSocket proxying\n  xfwd?: boolean; // Add x-forwarded-* headers\n  changeOrigin?: boolean; // Rewrite Host header to target\n  // ... 30+ more options for paths, headers, cookies, TLS, timeouts\n}\n```\n\n## Conventions\n\n- ESM only (`\"type\": \"module\"`)\n- Strict TypeScript with `nodenext` module resolution\n- Internal files prefixed with `_` (e.g., `_utils.ts`)\n- Tests use `vitest` + `expect.js` assertions\n- No production dependencies — Node.js built-ins only\n- Semantic commits: `feat(scope):`, `fix(scope):`, `test:`, `chore:`\n\n## Maintenance\n\n- Keep `AGENTS.md` and `README.md` in sync with the current codebase.\n- When you discover new behavior, constraints, architecture details, or workflows, document them in `AGENTS.md` for future agents.\n- When implementation or design changes affect user-facing usage or project expectations, update `README.md` in the same change.\n- Always add or update automated tests for behavior changes and bug fixes, including regression coverage that would fail without the change.\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# 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- **server:** `listeningCallback` ([#127](https://github.com/unjs/httpxy/pull/127))\n\n### 🩹 Fixes\n\n- Use `agents: false` for ws upgrade ([#129](https://github.com/unjs/httpxy/pull/129))\n- Do not set undefined values for headers ([#130](https://github.com/unjs/httpxy/pull/130))\n\n### 🏡 Chore\n\n- Apply automated updates ([b227f65](https://github.com/unjs/httpxy/commit/b227f65))\n- Update deps ([6f5ef12](https://github.com/unjs/httpxy/commit/6f5ef12))\n\n### ❤️ Contributors\n\n- Sukka <isukkaw@gmail.com>\n- Daniel Roe ([@danielroe](https://github.com/danielroe))\n- Pooya Parsa ([@pi0](https://github.com/pi0))\n\n## v0.5.0\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.4.0...v0.5.0)\n\n### 🔥 Performance\n\n- ⚠️ Keep-alive, request body stream and faster proxyFetch ([#124](https://github.com/unjs/httpxy/pull/124))\n\n### 🏡 Chore\n\n- Apply automated updates ([04aaaba](https://github.com/unjs/httpxy/commit/04aaaba))\n\n#### ⚠️ Breaking Changes\n\n- ⚠️ Keep-alive, request body stream and faster proxyFetch ([#124](https://github.com/unjs/httpxy/pull/124))\n\n### ❤️ Contributors\n\n- Pooya Parsa ([@pi0](https://github.com/pi0))\n\n## v0.4.0\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.3.1...v0.4.0)\n\n### 🚀 Enhancements\n\n- Http/2 listener support ([#102](https://github.com/unjs/httpxy/pull/102))\n- **fetch:** Add proxyFetch options for timeout, xfwd, changeOrigin, agent, followRedirects, HTTPS, and path merging ([efa9711](https://github.com/unjs/httpxy/commit/efa9711))\n\n### 🩹 Fixes\n\n- **web-incoming:** Close downstream stream when upstream SSE aborts ([#103](https://github.com/unjs/httpxy/pull/103))\n- Handle relative Location URLs in redirect rewriting ([#20](https://github.com/unjs/httpxy/pull/20), [#104](https://github.com/unjs/httpxy/pull/104))\n- **web-outgoing:** Handle invalid response header characters gracefully ([#106](https://github.com/unjs/httpxy/pull/106))\n- **web-incoming:** Remove deprecated `req.abort()` and `req.on(\"aborted\")` ([#107](https://github.com/unjs/httpxy/pull/107))\n- **web-outgoing:** Handle object target in redirect host rewrite ([#108](https://github.com/unjs/httpxy/pull/108))\n- **web-incoming:** Remove deprecated `req.on('aborted')` listener ([#110](https://github.com/unjs/httpxy/pull/110))\n- **ws:** Skip writing to closed socket on non-upgrade response ([#114](https://github.com/unjs/httpxy/pull/114))\n- **web-incoming:** Guard `req.socket` access in error handler ([#112](https://github.com/unjs/httpxy/pull/112))\n- **web-incoming:** Defer pipe until socket connects ([#111](https://github.com/unjs/httpxy/pull/111))\n- **server:** Catch synchronous exceptions in middleware passes ([#109](https://github.com/unjs/httpxy/pull/109))\n- **web-incoming:** Emit econnreset on client disconnect ([#115](https://github.com/unjs/httpxy/pull/115))\n- **ws:** Handle response stream errors on failed WS upgrade ([#116](https://github.com/unjs/httpxy/pull/116))\n- **web-outgoing:** Include HTTP 303 in redirect location rewriting ([#119](https://github.com/unjs/httpxy/pull/119))\n- **web-outgoing:** Skip empty header names ([#121](https://github.com/unjs/httpxy/pull/121))\n- **ssl:** Prevent undefined target values from overwriting ssl options ([#118](https://github.com/unjs/httpxy/pull/118))\n- **utils:** Preserve target URL query string in path merging ([#117](https://github.com/unjs/httpxy/pull/117))\n- **middleware:** Do not append duplicate x-forwarded-\\* header values ([#120](https://github.com/unjs/httpxy/pull/120))\n- **web-outgoing:** Strip transfer-encoding on 204/304 ([#122](https://github.com/unjs/httpxy/pull/122))\n- **web-incoming:** Use `isSSL` regex for consistent https/wss protocol checks ([#123](https://github.com/unjs/httpxy/pull/123))\n- **ws:** Preserve wss:// protocol and fix error handling in proxyUpgrade ([cb01605](https://github.com/unjs/httpxy/commit/cb01605))\n\n### 📦 Build\n\n- ⚠️ Esm-only ([d65b3f7](https://github.com/unjs/httpxy/commit/d65b3f7))\n\n### 🏡 Chore\n\n- Update deps ([743098d](https://github.com/unjs/httpxy/commit/743098d))\n\n#### ⚠️ Breaking Changes\n\n- ⚠️ Esm-only ([d65b3f7](https://github.com/unjs/httpxy/commit/d65b3f7))\n\n### ❤️ Contributors\n\n- Pooya Parsa ([@pi0](https://github.com/pi0))\n- Guoyangzhen <upgyz@qq.com>\n- Sukka <isukkaw@gmail.com>\n- Gabor Koos <gabor.koos@gmail.com>\n\n## v0.3.1\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.3.0...v0.3.1)\n\n### 🚀 Enhancements\n\n- Standalone `proxyUpgrade` util ([#100](https://github.com/unjs/httpxy/pull/100))\n\n### 🏡 Chore\n\n- Apply automated updates ([d8c97ee](https://github.com/unjs/httpxy/commit/d8c97ee))\n\n### ✅ Tests\n\n- Use stub objects ([2287e56](https://github.com/unjs/httpxy/commit/2287e56))\n\n### ❤️ Contributors\n\n- Pooya Parsa ([@pi0](https://github.com/pi0))\n\n## v0.3.0\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.2.2...v0.3.0)\n\n### 🚀 Enhancements\n\n- `proxyFetch` ([#98](https://github.com/unjs/httpxy/pull/98))\n- **web-incoming:** Implement native `followRedirects` support ([d3d7f39](https://github.com/unjs/httpxy/commit/d3d7f39))\n\n### 🩹 Fixes\n\n- **proxy:** Ensure leading slash on `toProxy` outgoing path ([7759c94](https://github.com/unjs/httpxy/commit/7759c94))\n- **server:** Emit proxy error when listener exists, reject only when unhandled ([c9d2c51](https://github.com/unjs/httpxy/commit/c9d2c51))\n- **web-incoming:** Destroy request socket on timeout ([40105be](https://github.com/unjs/httpxy/commit/40105be))\n- **utils:** Preserve multiple consecutive slashes in request URL ([18e4d0d](https://github.com/unjs/httpxy/commit/18e4d0d))\n- **web-incoming:** Abort proxy request when client disconnects ([a5d4996](https://github.com/unjs/httpxy/commit/a5d4996))\n- **ws:** Handle client socket errors before upstream upgrade ([aebb5c6](https://github.com/unjs/httpxy/commit/aebb5c6))\n\n### 💅 Refactors\n\n- ⚠️ Remove legacy node `Url` support ([b2e6c92](https://github.com/unjs/httpxy/commit/b2e6c92))\n\n### 🏡 Chore\n\n- Enable strict typescript with nodenext resolution ([0c147a3](https://github.com/unjs/httpxy/commit/0c147a3))\n- Format repo ([d7e707f](https://github.com/unjs/httpxy/commit/d7e707f))\n- Update readme ([24f8b1a](https://github.com/unjs/httpxy/commit/24f8b1a))\n- Add more examples for proxy fetch ([d0cb298](https://github.com/unjs/httpxy/commit/d0cb298))\n- Apply automated updates ([d666b65](https://github.com/unjs/httpxy/commit/d666b65))\n- Add agents.md ([f497cb0](https://github.com/unjs/httpxy/commit/f497cb0))\n- Apply automated updates ([9a8d8eb](https://github.com/unjs/httpxy/commit/9a8d8eb))\n- Apply automated updates ([822a0ea](https://github.com/unjs/httpxy/commit/822a0ea))\n- Lint ([2d556f9](https://github.com/unjs/httpxy/commit/2d556f9))\n- Update deps ([63b750f](https://github.com/unjs/httpxy/commit/63b750f))\n\n### ✅ Tests\n\n- Fix todo items ([8a3732b](https://github.com/unjs/httpxy/commit/8a3732b))\n- Increase coverage ([50c0929](https://github.com/unjs/httpxy/commit/50c0929))\n- Use random ports only ([9e2d155](https://github.com/unjs/httpxy/commit/9e2d155))\n\n### 🤖 CI\n\n- Update actions ([1fbac92](https://github.com/unjs/httpxy/commit/1fbac92))\n\n#### ⚠️ Breaking Changes\n\n- ⚠️ Remove legacy node `Url` support ([b2e6c92](https://github.com/unjs/httpxy/commit/b2e6c92))\n\n### ❤️ Contributors\n\n- Pooya Parsa ([@pi0](https://github.com/pi0))\n\n## v0.2.2\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.2.1...v0.2.2)\n\n### 🏡 Chore\n\n- Fix build script ([28dc9e6](https://github.com/unjs/httpxy/commit/28dc9e6))\n\n### ❤️ Contributors\n\n- Pooya Parsa ([@pi0](https://github.com/pi0))\n\n## v0.2.1\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.2.0...v0.2.1)\n\n### 🌊 Types\n\n- Make httpxy's server event type map generic ([#97](https://github.com/unjs/httpxy/pull/97))\n\n### 🏡 Chore\n\n- Update deps ([aecbed3](https://github.com/unjs/httpxy/commit/aecbed3))\n\n### ❤️ Contributors\n\n- Pooya Parsa ([@pi0](https://github.com/pi0))\n- Sukka <isukkaw@gmail.com>\n\n## v0.2.0\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.1.7...v0.2.0)\n\n### 💅 Refactors\n\n- ⚠️ Code improvements ([#78](https://github.com/unjs/httpxy/pull/78))\n\n### 🌊 Types\n\n- Implement typed proxy server event ([#95](https://github.com/unjs/httpxy/pull/95), [#96](https://github.com/unjs/httpxy/pull/96))\n\n### 🏡 Chore\n\n- Update dev dependencies ([81f5e57](https://github.com/unjs/httpxy/commit/81f5e57))\n- Migrate to oxfmt and oxlint ([edd6cff](https://github.com/unjs/httpxy/commit/edd6cff))\n\n### ✅ Tests\n\n- Port tests from node-http-proxy ([#88](https://github.com/unjs/httpxy/pull/88))\n\n#### ⚠️ Breaking Changes\n\n- ⚠️ Code improvements ([#78](https://github.com/unjs/httpxy/pull/78))\n\n### ❤️ Contributors\n\n- Pooya Parsa ([@pi0](https://github.com/pi0))\n- Sukka ([@SukkaW](https://github.com/SukkaW))\n- 翠 <green@sapphi.red>\n\n## v0.1.7\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.1.6...v0.1.7)\n\n### 🩹 Fixes\n\n- Preserve double slashes in url ([#70](https://github.com/unjs/httpxy/pull/70))\n\n### 🏡 Chore\n\n- Update deps ([c9c9de8](https://github.com/unjs/httpxy/commit/c9c9de8))\n\n### ❤️ Contributors\n\n- Oskar Lebuda ([@OskarLebuda](http://github.com/OskarLebuda))\n- Pooya Parsa ([@pi0](http://github.com/pi0))\n\n## v0.1.6\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.1.5...v0.1.6)\n\n### 🩹 Fixes\n\n- Omit outgoing port when not required ([#65](https://github.com/unjs/httpxy/pull/65))\n\n### 📖 Documentation\n\n- Remove unsupported `followRedirects` option ([#66](https://github.com/unjs/httpxy/pull/66))\n- Improve example ([#16](https://github.com/unjs/httpxy/pull/16))\n\n### 🏡 Chore\n\n- Fix typo in readme ([#36](https://github.com/unjs/httpxy/pull/36))\n- Update repo ([64f7465](https://github.com/unjs/httpxy/commit/64f7465))\n- Update ci ([b0f08de](https://github.com/unjs/httpxy/commit/b0f08de))\n\n### ❤️ Contributors\n\n- Lsh ([@peterroe](http://github.com/peterroe))\n- Kricsleo ([@kricsleo](http://github.com/kricsleo))\n- Pooya Parsa ([@pi0](http://github.com/pi0))\n- Mohammd Siddiqui <masiddiqui91@gmail.com>\n\n## v0.1.5\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.1.4...v0.1.5)\n\n### 🩹 Fixes\n\n- Handle client `close` event ([#8](https://github.com/unjs/httpxy/pull/8))\n\n### 🏡 Chore\n\n- Update deps ([2888089](https://github.com/unjs/httpxy/commit/2888089))\n\n### ❤️ Contributors\n\n- Pooya Parsa ([@pi0](http://github.com/pi0))\n- David Tai ([@didavid61202](http://github.com/didavid61202))\n\n## v0.1.4\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.1.2...v0.1.4)\n\n### 🩹 Fixes\n\n- Presrve search params from parsed url ([8bbaacc](https://github.com/unjs/httpxy/commit/8bbaacc))\n- Add `target` pathname currectly ([#6](https://github.com/unjs/httpxy/pull/6))\n\n### 💅 Refactors\n\n- Fix typo in `defineProxyMiddleware` ([#4](https://github.com/unjs/httpxy/pull/4))\n\n### 🏡 Chore\n\n- **release:** V0.1.2 ([b6bd4a8](https://github.com/unjs/httpxy/commit/b6bd4a8))\n- Update dev dependencies ([5704e70](https://github.com/unjs/httpxy/commit/5704e70))\n- **release:** V0.1.3 ([4ced1cc](https://github.com/unjs/httpxy/commit/4ced1cc))\n\n### ❤️ Contributors\n\n- Pooya Parsa ([@pi0](http://github.com/pi0))\n- Jonasolesen\n- Gacek1123\n\n## v0.1.3\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.1.2...v0.1.3)\n\n### 🩹 Fixes\n\n- Presrve search params from parsed url ([8bbaacc](https://github.com/unjs/httpxy/commit/8bbaacc))\n\n### 💅 Refactors\n\n- Fix typo in `defineProxyMiddleware` ([#4](https://github.com/unjs/httpxy/pull/4))\n\n### 🏡 Chore\n\n- **release:** V0.1.2 ([b6bd4a8](https://github.com/unjs/httpxy/commit/b6bd4a8))\n- Update dev dependencies ([a41d0c6](https://github.com/unjs/httpxy/commit/a41d0c6))\n\n### ❤️ Contributors\n\n- Pooya Parsa ([@pi0](http://github.com/pi0))\n- Gacek1123\n\n## v0.1.2\n\n[compare changes](https://github.com/unjs/httpxy/compare/v0.1.1...v0.1.2)\n\n### 🩹 Fixes\n\n- Presrve search params from parsed url ([8bbaacc](https://github.com/unjs/httpxy/commit/8bbaacc))\n\n### ❤️ Contributors\n\n- Pooya Parsa ([@pi0](http://github.com/pi0))\n\n## v0.1.1\n\n### 🚀 Enhancements\n\n- Awaitable `.`web`and`.ws` ([e4dad27](https://github.com/unjs/httpxy/commit/e4dad27))\n\n### 🩹 Fixes\n\n- `createProxyServer` options is optional ([75d8e93](https://github.com/unjs/httpxy/commit/75d8e93))\n\n### 💅 Refactors\n\n- Avoid `url.parse` ([4ceca85](https://github.com/unjs/httpxy/commit/4ceca85))\n- Hide internal props ([2f30878](https://github.com/unjs/httpxy/commit/2f30878))\n\n### 📖 Documentation\n\n- No need for quote ([9319fab](https://github.com/unjs/httpxy/commit/9319fab))\n\n### 🏡 Chore\n\n- Update readme ([64a7a75](https://github.com/unjs/httpxy/commit/64a7a75))\n- Update dependencies ([1e906b9](https://github.com/unjs/httpxy/commit/1e906b9))\n\n### ❤️ Contributors\n\n- Pooya Parsa ([@pi0](http://github.com/pi0))\n- Sébastien Chopin <seb@nuxtlabs.com>\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "@AGENTS.md\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) Pooya Parsa <pooya@pi0.io>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n----\n\nBased on http-party/node-http-proxy (9b96cd7)\n\nCopyright (c) 2010-2016 Charlie Robbins, Jarrett Cruger & the Contributors.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# 🔀 httpxy\n\n[![npm version][npm-version-src]][npm-version-href]\n[![npm downloads][npm-downloads-src]][npm-downloads-href]\n[![bundle][bundle-src]][bundle-href]\n[![Codecov][codecov-src]][codecov-href]\n\nA Full-Featured HTTP and WebSocket Proxy for Node.js\n\n## Proxy Fetch\n\n`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.\n\n```ts\nimport { proxyFetch } from \"httpxy\";\n\n// TCP — using a URL string\nconst res = await proxyFetch(\"http://127.0.0.1:3000\", \"http://example.com/api/data\");\nconsole.log(await res.json());\n\n// Unix socket — using a URL string\nconst res2 = await proxyFetch(\"unix:/tmp/app.sock\", \"http://localhost/health\");\nconsole.log(await res2.text());\n\n// Or use an object for more control\nconst res3 = await proxyFetch({ host: \"127.0.0.1\", port: 3000 }, \"http://example.com/api/data\");\n\n// Using a Request object\nconst req = new Request(\"http://example.com/api/data\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ key: \"value\" }),\n});\nconst res4 = await proxyFetch(\"http://127.0.0.1:3000\", req);\n\n// Using a URL string with RequestInit\nconst res5 = await proxyFetch(\"http://127.0.0.1:3000\", \"http://example.com/api/data\", {\n  method: \"PUT\",\n  headers: { Authorization: \"Bearer token\" },\n  body: JSON.stringify({ updated: true }),\n});\n```\n\nIt 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.\n\n## Proxy Upgrade\n\n`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`.\n\n```ts\nimport { createServer } from \"node:http\";\nimport { proxyUpgrade } from \"httpxy\";\n\nconst server = createServer((req, res) => {\n  // Handle regular HTTP requests...\n});\n\nserver.on(\"upgrade\", (req, socket, head) => {\n  proxyUpgrade(\"http://127.0.0.1:8080\", req, socket, head);\n});\n\nserver.listen(3000);\n```\n\nIt 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.\n\n```ts\n// With options\nserver.on(\"upgrade\", (req, socket, head) => {\n  proxyUpgrade({ host: \"127.0.0.1\", port: 8080 }, req, socket, head, {\n    // changeOrigin: true, // rewrite Host header\n    // xfwd: false, // disable x-forwarded-* headers (enabled by default)\n  });\n});\n```\n\n## Proxy Server\n\n> [!NOTE]\n> Proxy server was originally forked from [http-party/node-http-proxy](https://github.com/http-party/node-http-proxy).\n\nCreate proxy:\n\n```ts\nimport { createServer } from \"node:http\";\nimport { createProxyServer } from \"httpxy\";\n\nconst proxy = createProxyServer({});\n\nconst server = createServer(async (req, res) => {\n  try {\n    await proxy.web(req, res, {\n      target: address /* address of your proxy server here */,\n    });\n  } catch (error) {\n    console.error(error);\n    res.statusCode = 500;\n    res.end(\"Proxy error: \" + error.toString());\n  }\n});\n\nserver.listen(3000, () => {\n  console.log(\"Proxy is listening on http://localhost:3000\");\n});\n```\n\n## Options\n\n| Option                  | Type                                   | Default    | Description                                                                 |\n| ----------------------- | -------------------------------------- | ---------- | --------------------------------------------------------------------------- |\n| `target`                | `string \\| URL \\| ProxyTargetDetailed` | —          | Target server URL                                                           |\n| `forward`               | `string \\| URL`                        | —          | Forward server URL (pipes request without the target's response)            |\n| `agent`                 | `http.Agent \\| false`                  | keep-alive | Shared keep-alive agent by default. Set `false` to disable connection reuse |\n| `ssl`                   | `https.ServerOptions`                  | —          | Object passed to `https.createServer()`                                     |\n| `ws`                    | `boolean`                              | `false`    | Enable WebSocket proxying                                                   |\n| `xfwd`                  | `boolean`                              | `false`    | Add `x-forwarded-*` headers                                                 |\n| `secure`                | `boolean`                              | —          | Verify SSL certificates                                                     |\n| `toProxy`               | `boolean`                              | `false`    | Pass absolute URL as path (proxy-to-proxy)                                  |\n| `prependPath`           | `boolean`                              | `true`     | Prepend the target's path to the proxy path                                 |\n| `ignorePath`            | `boolean`                              | `false`    | Ignore the incoming request path                                            |\n| `localAddress`          | `string`                               | —          | Local interface to bind for outgoing connections                            |\n| `changeOrigin`          | `boolean`                              | `false`    | Change the `Host` header to match the target URL                            |\n| `preserveHeaderKeyCase` | `boolean`                              | `false`    | Keep original letter case of response header keys                           |\n| `auth`                  | `string`                               | —          | Basic authentication (`'user:password'`) for `Authorization` header         |\n| `hostRewrite`           | `string`                               | —          | Rewrite the `Location` hostname on redirects (301/302/307/308)              |\n| `autoRewrite`           | `boolean`                              | `false`    | Rewrite `Location` host/port on redirects based on the request              |\n| `protocolRewrite`       | `string`                               | —          | Rewrite `Location` protocol on redirects (`'http'` or `'https'`)            |\n| `cookieDomainRewrite`   | `false \\| string \\| object`            | `false`    | Rewrite domain of `Set-Cookie` headers                                      |\n| `cookiePathRewrite`     | `false \\| string \\| object`            | `false`    | Rewrite path of `Set-Cookie` headers                                        |\n| `headers`               | `object`                               | —          | Extra headers to add to target requests                                     |\n| `proxyTimeout`          | `number`                               | `120000`   | Timeout (ms) for the proxy request to the target                            |\n| `timeout`               | `number`                               | —          | Timeout (ms) for the incoming request                                       |\n| `selfHandleResponse`    | `boolean`                              | `false`    | Disable automatic response piping (handle `proxyRes` yourself)              |\n| `followRedirects`       | `boolean \\| number`                    | `false`    | Follow HTTP redirects from target. `true` = max 5 hops; number = custom max |\n| `buffer`                | `stream.Stream`                        | —          | Stream to use as request body instead of the incoming request               |\n\n## Events\n\n| Event        | Arguments                                | Description                                            |\n| ------------ | ---------------------------------------- | ------------------------------------------------------ |\n| `error`      | `(err, req, res, target)`                | An error occurred during proxying                      |\n| `proxyReq`   | `(proxyReq, req, res, options)`          | Before request is sent to target (modify headers here) |\n| `proxyRes`   | `(proxyRes, req, res)`                   | Response received from target                          |\n| `proxyReqWs` | `(proxyReq, req, socket, options, head)` | Before WebSocket upgrade request is sent               |\n| `open`       | `(proxySocket)`                          | WebSocket connection opened                            |\n| `close`      | `(proxyRes, proxySocket, proxyHead)`     | WebSocket connection closed                            |\n| `start`      | `(req, res, target)`                     | Proxy processing started                               |\n| `end`        | `(req, res, proxyRes)`                   | Proxy request completed                                |\n\n## Examples\n\n### HTTP Proxy\n\n```ts\nimport { createServer } from \"node:http\";\nimport { createProxyServer } from \"httpxy\";\n\nconst proxy = createProxyServer({});\n\nconst server = createServer(async (req, res) => {\n  await proxy.web(req, res, { target: \"http://localhost:8080\" });\n});\n\nserver.listen(3000);\n```\n\n### WebSocket Proxy\n\n```ts\nimport { createServer } from \"node:http\";\nimport { createProxyServer } from \"httpxy\";\n\nconst proxy = createProxyServer({ target: \"http://localhost:8080\", ws: true });\n\nconst server = createServer(async (req, res) => {\n  await proxy.web(req, res);\n});\n\nserver.on(\"upgrade\", (req, socket, head) => {\n  proxy.ws(req, socket, { target: \"http://localhost:8080\" }, head);\n});\n\nserver.listen(3000);\n```\n\n### Modify Request Headers\n\n```ts\nimport { createServer } from \"node:http\";\nimport { createProxyServer } from \"httpxy\";\n\nconst proxy = createProxyServer({ target: \"http://localhost:8080\" });\n\nproxy.on(\"proxyReq\", (proxyReq) => {\n  proxyReq.setHeader(\"X-Forwarded-By\", \"httpxy\");\n});\n\nconst server = createServer(async (req, res) => {\n  await proxy.web(req, res);\n});\n\nserver.listen(3000);\n```\n\n### HTTPS Proxy\n\n```ts\nimport { readFileSync } from \"node:fs\";\nimport { createProxyServer } from \"httpxy\";\n\nconst proxy = createProxyServer({\n  ssl: {\n    key: readFileSync(\"server-key.pem\", \"utf8\"),\n    cert: readFileSync(\"server-cert.pem\", \"utf8\"),\n  },\n  target: \"https://localhost:8443\",\n  secure: false, // allow self-signed certificates\n});\n\nproxy.listen(3000);\n```\n\n### Standalone Proxy Server\n\n```ts\nimport { createProxyServer } from \"httpxy\";\n\nconst proxy = createProxyServer({\n  target: \"http://localhost:8080\",\n  changeOrigin: true,\n});\n\nproxy.listen(3000);\n```\n\n## Development\n\n- Clone this repository\n- Install latest LTS version of [Node.js](https://nodejs.org/en/)\n- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`\n- Install dependencies using `pnpm install`\n- Run interactive tests using `pnpm dev`\n\n## Acknowledgements\n\nPerformance 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).\n\n## License\n\nMade with 💛\n\nPublished under [MIT License](./LICENSE).\n\n<!-- Badges -->\n\n[npm-version-src]: https://img.shields.io/npm/v/httpxy?style=flat&colorA=18181B&colorB=F0DB4F\n[npm-version-href]: https://npmjs.com/package/httpxy\n[npm-downloads-src]: https://img.shields.io/npm/dm/httpxy?style=flat&colorA=18181B&colorB=F0DB4F\n[npm-downloads-href]: https://npmjs.com/package/httpxy\n[codecov-src]: https://img.shields.io/codecov/c/gh/unjs/httpxy/main?style=flat&colorA=18181B&colorB=F0DB4F\n[codecov-href]: https://codecov.io/gh/unjs/httpxy\n[bundle-src]: https://img.shields.io/bundlephobia/minzip/httpxy?style=flat&colorA=18181B&colorB=F0DB4F\n[bundle-href]: https://bundlephobia.com/result?p=httpxy\n"
  },
  {
    "path": "bench/Dockerfile",
    "content": "FROM node:lts\n\nCOPY --from=alpine/bombardier /usr/bin/bombardier /usr/local/bin/bombardier\nRUN corepack enable && corepack prepare pnpm@latest --activate\n\nWORKDIR /app\nCOPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./\nCOPY bench/package.json bench/\nRUN pnpm install --frozen-lockfile\n\nCOPY src/ src/\nCOPY bench/ bench/\nCOPY tsconfig.json ./\n"
  },
  {
    "path": "bench/bench.ts",
    "content": "#!/usr/bin/env node\n\nimport { execSync, execFileSync, execFile as _execFile } from \"node:child_process\";\nimport { parseArgs } from \"node:util\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(_execFile);\n\n// --- Config ---\n\nconst { values: args } = parseArgs({\n  options: {\n    duration: { type: \"string\", short: \"d\", default: \"1s\" },\n    connections: { type: \"string\", short: \"c\", default: \"128\" },\n    sequential: { type: \"boolean\", short: \"s\", default: true },\n  },\n});\n\nconst IMAGE = \"httpxy-bench\";\nconst DURATION = args.duration!;\nconst CONNECTIONS = Number(args.connections);\nconst SEQUENTIAL = args.sequential!;\nconst POST_BODY = JSON.stringify({\n  message: \"hello world\".repeat(30),\n  ts: 1234567890,\n  padding: \"x\".repeat(1024 - 360),\n}); // ~1KB\nconst TARGET_PORT = 3000;\n\nconst PROXIES = [\n  { name: \"httpxy.server\", script: \"bench/src/httpxy-server.ts\", port: 3001 },\n  { name: \"httpxy.proxyFetch\", script: \"bench/src/httpxy-fetch.ts\", port: 3002 },\n  { name: \"fast-proxy\", script: \"bench/src/fast-proxy.ts\", port: 3003 },\n  { name: \"@fastify/http-proxy\", script: \"bench/src/fastify.ts\", port: 3004 },\n  { name: \"http-proxy-3\", script: \"bench/src/http-proxy-3.ts\", port: 3005 },\n  { name: \"http-proxy\", script: \"bench/src/http-proxy.ts\", port: 3006 },\n];\n\n// --- Helpers ---\n\nconst blue = (s: string) => `\\x1B[1;34m${s}\\x1B[0m`;\nconst green = (s: string) => `\\x1B[1;32m${s}\\x1B[0m`;\n// const red = (s: string) => `\\x1B[1;31m${s}\\x1B[0m`;\n\nconst info = (msg: string) => console.log(blue(`=> ${msg}`));\nconst ok = (msg: string) => console.log(green(`   ${msg}`));\n// const err = (msg: string) => console.log(red(`   ${msg}`));\n\nconst containers: string[] = [];\n\nfunction cleanup() {\n  info(\"Cleaning up...\");\n  if (containers.length === 0) return;\n  try {\n    execSync(`docker rm -f ${containers.join(\" \")}`, { stdio: \"ignore\" });\n  } catch {}\n  containers.length = 0;\n}\n\nfunction dockerRun(...args: string[]) {\n  return execFileSync(\"docker\", [\"run\", \"--rm\", \"--network\", \"host\", ...args], {\n    encoding: \"utf8\",\n  }).trim();\n}\n\nfunction startContainer(name: string, script: string, port: number) {\n  const cid = dockerRun(\n    \"-d\",\n    \"--name\",\n    name,\n    \"--cpus=1\",\n    \"--memory=256m\",\n    \"-e\",\n    `PORT=${port}`,\n    \"-e\",\n    `TARGET=http://127.0.0.1:${TARGET_PORT}`,\n    IMAGE,\n    \"node\",\n    script,\n  );\n  containers.push(cid);\n}\n\nlet bombCounter = 0;\n\nasync function bombJson(args: string[]): Promise<string> {\n  const name = `bench-bombardier-${process.pid}-${bombCounter++}`;\n  const { stdout } = await execFileAsync(\n    \"docker\",\n    [\n      \"run\",\n      \"--rm\",\n      \"--network\",\n      \"host\",\n      \"--name\",\n      name,\n      IMAGE,\n      \"bombardier\",\n      \"--format=json\",\n      \"--print=result\",\n      \"--latencies\",\n      ...args,\n    ],\n    { encoding: \"utf8\" },\n  );\n  return stdout;\n}\n\nfunction waitForReady(port: number, retries = 60) {\n  for (let i = 0; i < retries; i++) {\n    try {\n      execSync(`curl -sf -o /dev/null http://127.0.0.1:${port}/`, { stdio: \"ignore\" });\n      return;\n    } catch {\n      execSync(\"sleep 0.3\", { stdio: \"ignore\" });\n    }\n  }\n  throw new Error(`Timed out waiting for port ${port}`);\n}\n\n// --- Bombardier types ---\n\ninterface BombardierResult {\n  bytesRead: number;\n  bytesWritten: number;\n  timeTakenSeconds: number;\n  req1xx: number;\n  req2xx: number;\n  req3xx: number;\n  req4xx: number;\n  req5xx: number;\n  others: number;\n  errors?: { description: string; count: number }[];\n  latency: {\n    mean: number; // nanoseconds\n    stddev: number;\n    max: number;\n    percentiles: Record<string, number>; // \"50\", \"75\", \"90\", \"95\", \"99\"\n  };\n  rps: {\n    mean: number;\n    stddev: number;\n    max: number;\n    percentiles: Record<string, number>;\n  };\n}\n\ninterface BenchResult {\n  rps: number;\n  avgLatency: number; // ns\n  p50: number; // ns\n  p99: number; // ns\n  bytesPerSec: number;\n}\n\n// --- Result parsing ---\n\nfunction formatNs(ns: number): string {\n  return ns < 1e6 ? `${(ns / 1e3).toFixed(0)}µs` : `${(ns / 1e6).toFixed(2)}ms`;\n}\n\nfunction formatThroughput(bytesPerSec: number): string {\n  return bytesPerSec > 1e6\n    ? `${(bytesPerSec / 1e6).toFixed(1)}MB/s`\n    : `${(bytesPerSec / 1e3).toFixed(0)}KB/s`;\n}\n\nfunction formatResult(r: BenchResult): string {\n  return `${r.rps.toFixed(0)} req/s | ${formatNs(r.avgLatency)} avg | ${formatThroughput(r.bytesPerSec)}`;\n}\n\nfunction parseResult(json: string): BenchResult {\n  const { result: r } = JSON.parse(json) as { result: BombardierResult };\n\n  const nonOk = r.req1xx + r.req3xx + r.req4xx + r.req5xx + r.others;\n  if (nonOk > 0) {\n    throw new Error(\n      `Non-2xx responses: 1xx=${r.req1xx} 3xx=${r.req3xx} 4xx=${r.req4xx} 5xx=${r.req5xx} other=${r.others}`,\n    );\n  }\n  if (r.errors && r.errors.length > 0) {\n    const details = r.errors.map((e) => `${e.description}(${e.count})`).join(\", \");\n    throw new Error(`Transport errors: ${details}`);\n  }\n\n  return {\n    rps: r.rps.mean,\n    avgLatency: r.latency.mean,\n    p50: r.latency.percentiles[\"50\"] ?? 0,\n    p99: r.latency.percentiles[\"99\"] ?? 0,\n    bytesPerSec: r.bytesRead / r.timeTakenSeconds,\n  };\n}\n\nconst HEADERS = [\"Proxy\", \"Req/s\", \"Scale\", \"Avg\", \"P50\", \"P99\", \"Throughput\"];\n\nfunction printTable(title: string, results: [name: string, result: BenchResult][]) {\n  // Sort by req/s descending\n  results.sort((a, b) => b[1].rps - a[1].rps);\n  const bestRps = Math.max(...results.map(([, r]) => r.rps));\n\n  // Build rows\n  const rows = results.map(([name, r]) => {\n    const ratio = bestRps > 0 ? r.rps / bestRps : 0;\n    return [\n      name,\n      r.rps.toFixed(0),\n      ratio >= 1 ? \"1.00x\" : `${ratio.toFixed(2)}x`,\n      formatNs(r.avgLatency),\n      formatNs(r.p50),\n      formatNs(r.p99),\n      formatThroughput(r.bytesPerSec),\n    ];\n  });\n\n  // Compute column widths from headers + data\n  const colWidths = HEADERS.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i]!.length)));\n\n  const mdRow = (cells: string[]) =>\n    `| ${cells.map((c, i) => (i === 0 ? c.padEnd(colWidths[i]!) : c.padStart(colWidths[i]!))).join(\" | \")} |`;\n\n  console.log();\n  console.log(`### ${title}`);\n  console.log();\n  console.log(mdRow(HEADERS));\n  console.log(\n    `| ${colWidths.map((w, i) => (i === 0 ? `:${\"-\".repeat(w - 1)}` : `${\"-\".repeat(w - 1)}:`)).join(\" | \")} |`,\n  );\n  for (const row of rows) {\n    console.log(mdRow(row));\n  }\n}\n\n// --- Main ---\n\nprocess.on(\"exit\", cleanup);\nprocess.on(\"SIGINT\", () => process.exit(1));\nprocess.on(\"SIGTERM\", () => process.exit(1));\n\ninfo(\"Running validation tests...\");\nexecSync(\"node bench/test.ts\", {\n  stdio: \"inherit\",\n  cwd: `${import.meta.dirname}/..`,\n});\nok(\"All implementations valid\");\n\ninfo(\"Building image...\");\nexecSync(`docker build -t ${IMAGE} -f bench/Dockerfile .`, {\n  stdio: \"inherit\",\n  cwd: `${import.meta.dirname}/..`,\n});\n\ninfo(\"Starting target server...\");\nstartContainer(\"bench-target\", \"bench/src/target.ts\", TARGET_PORT);\nwaitForReady(TARGET_PORT);\nok(\"target ready\");\n\ninfo(\"Starting proxy servers...\");\nfor (const { name, script, port } of PROXIES) {\n  const containerName = `bench-${name.replaceAll(\" \", \"-\").replaceAll(/[@/]/g, \"\")}`;\n  startContainer(containerName, script, port);\n}\n\nfor (const { name, port } of PROXIES) {\n  waitForReady(port);\n  ok(`${name} ready`);\n}\nconsole.log();\n\nasync function runBench(label: string, extraArgs: string[] = []) {\n  info(\n    `Benchmarking ${label} (duration=${DURATION}, connections=${CONNECTIONS}${SEQUENTIAL ? \", sequential\" : \"\"})`,\n  );\n  const benchOne = async ({ name, port }: (typeof PROXIES)[number]) => {\n    const json = await bombJson([\n      \"-c\",\n      String(CONNECTIONS),\n      \"-d\",\n      DURATION,\n      ...extraArgs,\n      `http://127.0.0.1:${port}/`,\n    ]);\n    const result = parseResult(json);\n    ok(`${name} — ${formatResult(result)}`);\n    return [name, result] as [string, BenchResult];\n  };\n  let results: [string, BenchResult][];\n  if (SEQUENTIAL) {\n    results = [];\n    for (const proxy of PROXIES) {\n      results.push(await benchOne(proxy));\n    }\n  } else {\n    results = await Promise.all(PROXIES.map(benchOne));\n  }\n  console.log();\n  return results;\n}\n\nconst getResults = await runBench(\"GET\");\nconst postResults = await runBench(\"POST ~1KB JSON\", [\n  \"-m\",\n  \"POST\",\n  \"-H\",\n  \"Content-Type: application/json\",\n  \"-b\",\n  POST_BODY,\n]);\n\n// --- Summary ---\n\nconsole.log();\ninfo(\"Summary\");\nconsole.log();\nconsole.log(\n  `> Duration: **${DURATION}** | Connections: **${CONNECTIONS}** | Mode: **${SEQUENTIAL ? \"sequential\" : \"parallel\"}**`,\n);\n\nprintTable(\"GET (no body)\", getResults);\nconsole.log();\nprintTable(\"POST (~1KB JSON)\", postResults);\nconsole.log();\ninfo(\"Done!\");\n"
  },
  {
    "path": "bench/package.json",
    "content": "{\n  \"name\": \"bench\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"devDependencies\": {\n    \"@fastify/http-proxy\": \"^11.4.2\",\n    \"@types/http-proxy\": \"^1.17.17\",\n    \"fast-proxy\": \"^2.2.0\",\n    \"fastify\": \"^5.8.4\",\n    \"http-proxy\": \"^1.18.1\",\n    \"http-proxy-3\": \"^1.23.2\",\n    \"mitata\": \"^1.0.34\"\n  }\n}\n"
  },
  {
    "path": "bench/src/fast-proxy.ts",
    "content": "import http from \"node:http\";\nimport fastProxy from \"fast-proxy\";\n\nconst PORT = Number(process.env.PORT) || 3003;\nconst TARGET = process.env.TARGET || \"http://target:3000\";\n\nconst { proxy } = fastProxy({ base: TARGET });\nconst server = http.createServer((req, res) => {\n  proxy(req, res, req.url!, {});\n});\n\nserver.listen(PORT, \"0.0.0.0\", () => {\n  console.log(`fast-proxy listening on :${PORT} -> ${TARGET}`);\n});\n"
  },
  {
    "path": "bench/src/fastify.ts",
    "content": "import Fastify from \"fastify\";\nimport httpProxy from \"@fastify/http-proxy\";\n\nconst PORT = Number(process.env.PORT) || 3004;\nconst TARGET = process.env.TARGET || \"http://target:3000\";\n\nconst app = Fastify();\nawait app.register(httpProxy, { upstream: TARGET });\nawait app.listen({ port: PORT, host: \"0.0.0.0\" });\nconsole.log(`@fastify/http-proxy listening on :${PORT} -> ${TARGET}`);\n"
  },
  {
    "path": "bench/src/http-proxy-3.ts",
    "content": "import http from \"node:http\";\nimport { createProxyServer } from \"http-proxy-3\";\n\nconst PORT = Number(process.env.PORT) || 3005;\nconst TARGET = process.env.TARGET || \"http://target:3000\";\n\nconst proxy = createProxyServer({ target: TARGET });\nconst server = http.createServer((req, res) => {\n  proxy.web(req, res);\n});\n\nserver.listen(PORT, \"0.0.0.0\", () => {\n  console.log(`http-proxy-3 listening on :${PORT} -> ${TARGET}`);\n});\n"
  },
  {
    "path": "bench/src/http-proxy.ts",
    "content": "import http from \"node:http\";\nimport httpProxy from \"http-proxy\";\n\nconst PORT = Number(process.env.PORT) || 3006;\nconst TARGET = process.env.TARGET || \"http://target:3000\";\n\nconst proxy = httpProxy.createProxyServer({ target: TARGET });\nconst server = http.createServer((req, res) => {\n  proxy.web(req, res);\n});\n\nserver.listen(PORT, \"0.0.0.0\", () => {\n  console.log(`http-proxy listening on :${PORT} -> ${TARGET}`);\n});\n"
  },
  {
    "path": "bench/src/httpxy-fetch.ts",
    "content": "import http from \"node:http\";\nimport { proxyFetch } from \"../../src/index.ts\";\n\nconst PORT = Number(process.env.PORT) || 3002;\nconst TARGET = process.env.TARGET || \"http://target:3000\";\n\nfunction collectBody(req: http.IncomingMessage): Promise<Buffer | undefined> {\n  if (req.method === \"GET\" || req.method === \"HEAD\") {\n    return Promise.resolve(undefined);\n  }\n  return new Promise((resolve) => {\n    const chunks: Buffer[] = [];\n    req.on(\"data\", (c: Buffer) => chunks.push(c));\n    req.on(\"end\", () => resolve(chunks.length > 0 ? Buffer.concat(chunks) : undefined));\n  });\n}\n\nconst server = http.createServer(async (req, res) => {\n  const body = await collectBody(req);\n  const response = await proxyFetch(TARGET, new URL(req.url!, `http://127.0.0.1:${PORT}`), {\n    method: req.method,\n    headers: req.headers as HeadersInit,\n    body: body as any,\n  });\n  res.writeHead(response.status, Object.fromEntries(response.headers));\n  if (response.body) {\n    for await (const chunk of response.body) {\n      res.write(chunk);\n    }\n  }\n  res.end();\n});\n\nserver.listen(PORT, \"0.0.0.0\", () => {\n  console.log(`httpxy proxyFetch proxy listening on :${PORT} -> ${TARGET}`);\n});\n"
  },
  {
    "path": "bench/src/httpxy-server.ts",
    "content": "import http from \"node:http\";\nimport { createProxyServer } from \"../../src/index.ts\";\n\nconst PORT = Number(process.env.PORT) || 3001;\nconst TARGET = process.env.TARGET || \"http://target:3000\";\n\nconst proxy = createProxyServer({ target: TARGET });\nconst server = http.createServer((req, res) => {\n  proxy.web(req, res);\n});\n\nserver.listen(PORT, \"0.0.0.0\", () => {\n  console.log(`httpxy server proxy listening on :${PORT} -> ${TARGET}`);\n});\n"
  },
  {
    "path": "bench/src/target.ts",
    "content": "import http from \"node:http\";\n\nconst PORT = Number(process.env.PORT) || 3000;\n\nconst server = http.createServer((req, res) => {\n  if (req.method === \"GET\") {\n    res.writeHead(200, { \"content-type\": \"application/json\" });\n    res.end('{\"ok\":true}');\n    return;\n  }\n  const chunks: Buffer[] = [];\n  req.on(\"data\", (c) => chunks.push(c));\n  req.on(\"end\", () => {\n    const body = Buffer.concat(chunks);\n    res.writeHead(200, {\n      \"content-type\": req.headers[\"content-type\"] || \"application/octet-stream\",\n      \"content-length\": String(body.length),\n    });\n    res.end(body);\n  });\n});\n\nserver.listen(PORT, \"0.0.0.0\", () => {\n  console.log(`target listening on :${PORT}`);\n});\n"
  },
  {
    "path": "bench/test.ts",
    "content": "#!/usr/bin/env node\nimport http from \"node:http\";\nimport { createProxyServer, proxyFetch } from \"../src/index.ts\";\nimport fastProxy from \"fast-proxy\";\nimport { createProxyServer as createHttpProxy3 } from \"http-proxy-3\";\nimport httpProxyLegacy from \"http-proxy\";\nimport Fastify from \"fastify\";\nimport httpProxy from \"@fastify/http-proxy\";\n\n// --- Config ---\n\nconst TARGET_PORT = 9_900;\nconst HTTPXY_SERVER_PORT = 9_901;\nconst HTTPXY_FETCH_PORT = 9_902;\nconst FAST_PROXY_PORT = 9_903;\nconst FASTIFY_PROXY_PORT = 9_904;\nconst HTTP_PROXY_3_PORT = 9_905;\nconst HTTP_PROXY_PORT = 9_906;\n\nconst SMALL_BODY = JSON.stringify({ message: \"hello world\", ts: Date.now() });\nconst LARGE_BODY = JSON.stringify({\n  data: Array.from({ length: 1000 }, (_, i) => ({\n    id: i,\n    name: `item-${i}`,\n    value: Math.random(),\n    tags: [\"a\", \"b\", \"c\"],\n  })),\n});\n\n// --- Target server (echo) ---\n\nfunction createTargetServer(): Promise<http.Server> {\n  return new Promise((resolve) => {\n    const server = http.createServer((req, res) => {\n      if (req.method === \"GET\") {\n        res.writeHead(200, { \"content-type\": \"application/json\" });\n        res.end('{\"ok\":true}');\n        return;\n      }\n      const chunks: Buffer[] = [];\n      req.on(\"data\", (c) => chunks.push(c));\n      req.on(\"end\", () => {\n        res.writeHead(200, {\n          \"content-type\": req.headers[\"content-type\"] || \"application/octet-stream\",\n          \"content-length\": String(Buffer.concat(chunks).length),\n        });\n        res.end(Buffer.concat(chunks));\n      });\n    });\n    server.listen(TARGET_PORT, () => resolve(server));\n  });\n}\n\n// --- Proxy servers setup ---\n\nconst TARGET = `http://127.0.0.1:${TARGET_PORT}`;\n\nasync function setupHttpxyServer(): Promise<http.Server> {\n  const proxy = createProxyServer({ target: TARGET });\n  const server = http.createServer((req, res) => {\n    proxy.web(req, res);\n  });\n  return new Promise((resolve) => {\n    server.listen(HTTPXY_SERVER_PORT, () => resolve(server));\n  });\n}\n\nfunction collectBody(req: http.IncomingMessage): Promise<Buffer | undefined> {\n  if (req.method === \"GET\" || req.method === \"HEAD\") {\n    return Promise.resolve(undefined);\n  }\n  return new Promise((resolve) => {\n    const chunks: Buffer[] = [];\n    req.on(\"data\", (c: Buffer) => chunks.push(c));\n    req.on(\"end\", () => resolve(chunks.length > 0 ? Buffer.concat(chunks) : undefined));\n  });\n}\n\nasync function setupHttpxyFetchServer(): Promise<http.Server> {\n  const server = http.createServer(async (req, res) => {\n    const body = await collectBody(req);\n    const response = await proxyFetch(\n      TARGET,\n      new URL(req.url!, `http://127.0.0.1:${HTTPXY_FETCH_PORT}`),\n      {\n        method: req.method,\n        headers: req.headers as HeadersInit,\n        body: body as any,\n      },\n    );\n    res.writeHead(response.status, Object.fromEntries(response.headers));\n    if (response.body) {\n      for await (const chunk of response.body) {\n        res.write(chunk);\n      }\n    }\n    res.end();\n  });\n  return new Promise((resolve) => {\n    server.listen(HTTPXY_FETCH_PORT, () => resolve(server));\n  });\n}\n\nasync function setupFastProxy(): Promise<{ server: http.Server; close: () => void }> {\n  const { proxy, close } = fastProxy({ base: TARGET });\n  const server = http.createServer((req, res) => {\n    proxy(req, res, req.url!, {});\n  });\n  return new Promise((resolve) => {\n    server.listen(FAST_PROXY_PORT, () => resolve({ server, close }));\n  });\n}\n\nasync function setupFastifyProxy(): Promise<ReturnType<typeof Fastify>> {\n  const app = Fastify();\n  await app.register(httpProxy, { upstream: TARGET });\n  await app.listen({ port: FASTIFY_PROXY_PORT });\n  return app;\n}\n\nasync function setupHttpProxy3(): Promise<http.Server> {\n  const proxy = createHttpProxy3({ target: TARGET });\n  const server = http.createServer((req, res) => {\n    proxy.web(req, res);\n  });\n  return new Promise((resolve) => {\n    server.listen(HTTP_PROXY_3_PORT, () => resolve(server));\n  });\n}\n\nasync function setupHttpProxyLegacy(): Promise<http.Server> {\n  const proxy = httpProxyLegacy.createProxyServer({ target: TARGET });\n  const server = http.createServer((req, res) => {\n    proxy.web(req, res);\n  });\n  return new Promise((resolve) => {\n    server.listen(HTTP_PROXY_PORT, () => resolve(server));\n  });\n}\n\n// --- HTTP helpers ---\n\ninterface HttpResult {\n  status: number;\n  headers: http.IncomingHttpHeaders;\n  body: string;\n}\n\nfunction httpGet(port: number, path = \"/\"): Promise<HttpResult> {\n  return new Promise((resolve, reject) => {\n    const req = http.get(`http://127.0.0.1:${port}${path}`, (res) => {\n      const chunks: Buffer[] = [];\n      res.on(\"data\", (c) => chunks.push(c));\n      res.on(\"end\", () =>\n        resolve({\n          status: res.statusCode!,\n          headers: res.headers,\n          body: Buffer.concat(chunks).toString(),\n        }),\n      );\n    });\n    req.on(\"error\", reject);\n  });\n}\n\nfunction httpPost(port: number, body: string, path = \"/\"): Promise<HttpResult> {\n  return new Promise((resolve, reject) => {\n    const req = http.request(\n      {\n        hostname: \"127.0.0.1\",\n        port,\n        path,\n        method: \"POST\",\n        headers: {\n          \"content-type\": \"application/json\",\n          \"content-length\": Buffer.byteLength(body),\n        },\n      },\n      (res) => {\n        const chunks: Buffer[] = [];\n        res.on(\"data\", (c) => chunks.push(c));\n        res.on(\"end\", () =>\n          resolve({\n            status: res.statusCode!,\n            headers: res.headers,\n            body: Buffer.concat(chunks).toString(),\n          }),\n        );\n      },\n    );\n    req.on(\"error\", reject);\n    req.end(body);\n  });\n}\n\n// --- Main ---\n\nasync function main() {\n  console.log(\"Starting servers...\");\n\n  const targetServer = await createTargetServer();\n  const httpxyServer = await setupHttpxyServer();\n  const httpxyFetchServer = await setupHttpxyFetchServer();\n  const fastProxySetup = await setupFastProxy();\n  const fastifyApp = await setupFastifyProxy();\n  const httpProxy3Server = await setupHttpProxy3();\n  const httpProxyLegacyServer = await setupHttpProxyLegacy();\n\n  console.log(\"Validating proxy implementations...\");\n\n  const proxies = [\n    { name: \"httpxy server\", port: HTTPXY_SERVER_PORT },\n    { name: \"httpxy proxyFetch\", port: HTTPXY_FETCH_PORT },\n    { name: \"fast-proxy\", port: FAST_PROXY_PORT },\n    { name: \"@fastify/http-proxy\", port: FASTIFY_PROXY_PORT },\n    { name: \"http-proxy-3\", port: HTTP_PROXY_3_PORT },\n    { name: \"http-proxy\", port: HTTP_PROXY_PORT },\n  ];\n\n  let allValid = true;\n\n  for (const { name, port } of proxies) {\n    const errors: string[] = [];\n\n    const getRes = await httpGet(port);\n    if (getRes.status !== 200) {\n      errors.push(`GET status=${getRes.status}, expected 200`);\n    }\n    if (getRes.body !== '{\"ok\":true}') {\n      errors.push(`GET body=${JSON.stringify(getRes.body)}, expected '{\"ok\":true}'`);\n    }\n\n    const postSmall = await httpPost(port, SMALL_BODY);\n    if (postSmall.status !== 200) {\n      errors.push(`POST(1KB) status=${postSmall.status}, expected 200`);\n    }\n    if (postSmall.body !== SMALL_BODY) {\n      errors.push(\n        `POST(1KB) body mismatch: got ${postSmall.body.length} bytes, expected ${SMALL_BODY.length}`,\n      );\n    }\n\n    const postLarge = await httpPost(port, LARGE_BODY);\n    if (postLarge.status !== 200) {\n      errors.push(`POST(100KB) status=${postLarge.status}, expected 200`);\n    }\n    if (postLarge.body !== LARGE_BODY) {\n      errors.push(\n        `POST(100KB) body mismatch: got ${postLarge.body.length} bytes, expected ${LARGE_BODY.length}`,\n      );\n    }\n\n    if (errors.length > 0) {\n      allValid = false;\n      console.log(`  FAIL  ${name}`);\n      for (const e of errors) {\n        console.log(`        - ${e}`);\n      }\n    } else {\n      console.log(`  OK    ${name}`);\n    }\n  }\n\n  // Cleanup\n  targetServer.close();\n  httpxyServer.close();\n  httpxyFetchServer.close();\n  fastProxySetup.server.close();\n  fastProxySetup.close();\n  await fastifyApp.close();\n  httpProxy3Server.close();\n  httpProxyLegacyServer.close();\n\n  if (!allValid) {\n    console.error(\"\\nValidation failed.\");\n    process.exit(1);\n  }\n\n  console.log(\"\\nAll implementations valid.\");\n}\n\nmain().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "build.config.mjs",
    "content": "import { defineBuildConfig } from \"obuild/config\";\n\nexport default defineBuildConfig({\n  entries: [\n    {\n      type: \"bundle\",\n      input: [\"./src/index.ts\"],\n    },\n  ],\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"httpxy\",\n  \"version\": \"0.5.1\",\n  \"description\": \"A full-featured HTTP proxy for Node.js.\",\n  \"license\": \"MIT\",\n  \"repository\": \"unjs/httpxy\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"types\": \"./dist/index.d.mts\",\n  \"exports\": {\n    \".\": \"./dist/index.mjs\"\n  },\n  \"scripts\": {\n    \"build\": \"obuild\",\n    \"dev\": \"vitest\",\n    \"lint\": \"oxlint . && oxfmt --check\",\n    \"fmt\": \"oxlint . --fix && oxfmt\",\n    \"prepack\": \"pnpm run build\",\n    \"release\": \"pnpm test && pnpm build && changelogen --release && npm publish && git push --follow-tags\",\n    \"test\": \"pnpm lint && pnpm typecheck && vitest run --coverage\",\n    \"typecheck\": \"tsgo --noEmit\"\n  },\n  \"devDependencies\": {\n    \"@types/async\": \"^3.2.25\",\n    \"@types/concat-stream\": \"^2.0.3\",\n    \"@types/express\": \"^5.0.6\",\n    \"@types/node\": \"^25.6.0\",\n    \"@types/semver\": \"^7.7.1\",\n    \"@types/sse\": \"^0.0.0\",\n    \"@types/ws\": \"^8.18.1\",\n    \"@typescript/native-preview\": \"^7.0.0-dev.20260421.1\",\n    \"@vitest/coverage-v8\": \"^4.1.5\",\n    \"async\": \"^3.2.6\",\n    \"changelogen\": \"^0.6.2\",\n    \"concat-stream\": \"^2.0.0\",\n    \"eslint-config-unjs\": \"^0.6.2\",\n    \"expect.js\": \"^0.3.1\",\n    \"obuild\": \"^0.4.33\",\n    \"ofetch\": \"^1.5.1\",\n    \"oxfmt\": \"^0.46.0\",\n    \"oxlint\": \"^1.61.0\",\n    \"semver\": \"^7.7.4\",\n    \"socket.io\": \"^4.8.3\",\n    \"socket.io-client\": \"^4.8.3\",\n    \"sse\": \"^0.0.8\",\n    \"typescript\": \"^6.0.3\",\n    \"undici\": \"^8.1.0\",\n    \"vitest\": \"^4.1.5\",\n    \"ws\": \"^8.20.0\"\n  },\n  \"packageManager\": \"pnpm@10.33.0\"\n}\n"
  },
  {
    "path": "playground/index.ts",
    "content": "import http from \"node:http\";\nimport { createProxyServer } from \"../src/index.ts\";\n\nasync function main() {\n  const main = http.createServer((req, res) => {\n    res.end(\n      JSON.stringify({\n        method: req.method,\n        path: req.url,\n        headers: req.headers,\n      }),\n    );\n  });\n  await new Promise<void>((resolve) => {\n    main.listen(3000, \"127.0.0.1\", resolve);\n  });\n\n  const httpProxy = createProxyServer();\n\n  const proxy = http.createServer(async (req, res) => {\n    try {\n      await httpProxy.web(req, res, {\n        target: \"http://127.0.0.1:3000\",\n      });\n    } catch (error) {\n      console.error(error);\n      res.statusCode = 500;\n      res.end(\"Proxy error: \" + (error as Error).toString());\n    }\n  });\n  await new Promise<void>((resolve) => {\n    proxy.listen(3001, \"127.0.0.1\", resolve);\n  });\n\n  console.log(\"main: http://127.0.0.1:3000\");\n  console.log(\"proxy: http://127.0.0.1:3001\");\n}\n\n// eslint-disable-next-line unicorn/prefer-top-level-await\nmain();\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - bench\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"extends\": [\"github>unjs/renovate-config\"]\n}\n"
  },
  {
    "path": "src/_utils.ts",
    "content": "import httpNative from \"node:http\";\nimport httpsNative from \"node:https\";\nimport net from \"node:net\";\nimport type { ProxyAddr, ProxyServerOptions, ProxyTarget, ProxyTargetDetailed } from \"./types.ts\";\nimport type { Http2ServerRequest } from \"node:http2\";\n\nconst upgradeHeader = /(^|,)\\s*upgrade\\s*($|,)/i;\n\n/**\n * Default keep-alive agents for connection reuse.\n */\nexport const defaultAgents = {\n  http: new httpNative.Agent({ keepAlive: true, maxSockets: 256, maxFreeSockets: 64 }),\n  https: new httpsNative.Agent({ keepAlive: true, maxSockets: 256, maxFreeSockets: 64 }),\n};\n\n/**\n * Simple Regex for testing if protocol is https\n */\nexport const isSSL = /^https|wss/;\n\n/**\n * Node.js HTTP/2 accepts pseudo headers and it may conflict\n * with request options.\n *\n * Let's just blacklist those potential conflicting pseudo\n * headers.\n */\nconst HTTP2_HEADER_BLACKLIST = [\":method\", \":path\", \":scheme\", \":authority\"];\n\n/**\n * Copies the right headers from `options` and `req` to\n * `outgoing` which is then used to fire the proxied\n * request.\n *\n * Examples:\n *\n *    common.setupOutgoing(outgoing, options, req)\n *    // => { host: ..., hostname: ...}\n *\n * @param outgoing Base object to be filled with required properties\n * @param options Config object passed to the proxy\n * @param req Request Object\n * @param forward String to select forward or target\n *\n * @return Outgoing Object with all required properties set\n *\n * @api private\n */\nexport function setupOutgoing(\n  outgoing: httpNative.RequestOptions & httpsNative.RequestOptions,\n  options: ProxyServerOptions & {\n    target: ProxyTarget;\n    forward?: ProxyTarget;\n    ca?: string;\n    method?: string;\n  },\n  req: httpNative.IncomingMessage | Http2ServerRequest,\n  forward?: \"forward\" | \"target\",\n): httpNative.RequestOptions | httpsNative.RequestOptions {\n  outgoing.port =\n    (options[forward || \"target\"] as URL).port ||\n    (isSSL.test((options[forward || \"target\"] as URL).protocol ?? \"http\") ? 443 : 80);\n\n  for (const e of [\n    \"host\",\n    \"hostname\",\n    \"socketPath\",\n    \"pfx\",\n    \"key\",\n    \"passphrase\",\n    \"cert\",\n    \"ca\",\n    \"ciphers\",\n    \"secureProtocol\",\n  ] as const) {\n    const value = (options[forward || \"target\"] as ProxyTargetDetailed)[e];\n    if (value !== undefined) {\n      outgoing[e] = value as any;\n    }\n  }\n\n  outgoing.method = options.method || req.method;\n  outgoing.headers = { ...req.headers };\n\n  // before clean up HTTP/2 blacklist header, we might wanna override host first\n  if (req.headers?.[\":authority\"]) {\n    outgoing.headers.host = req.headers[\":authority\"] as string;\n  }\n  // host override must happen before composing/merging the final outgoing headers\n\n  if (options.headers) {\n    for (const key of Object.keys(options.headers)) {\n      outgoing.headers[key] = options.headers[key];\n    }\n  }\n\n  if (req.httpVersionMajor > 1) {\n    // ignore potential conflicting HTTP/2 pseudo headers\n    for (const header of HTTP2_HEADER_BLACKLIST) {\n      delete outgoing.headers[header];\n    }\n  }\n\n  if (options.auth) {\n    outgoing.auth = options.auth;\n  }\n\n  if (options.ca) {\n    outgoing.ca = options.ca;\n  }\n\n  if (isSSL.test((options[forward || \"target\"] as URL).protocol ?? \"http\")) {\n    outgoing.rejectUnauthorized = options.secure === undefined ? true : options.secure;\n  }\n\n  if (options.agent !== undefined) {\n    outgoing.agent = options.agent || false;\n  } else if (req.httpVersionMajor > 1 || upgradeHeader.test(req.headers.connection || \"\")) {\n    // WebSocket upgrades and HTTP/2 incoming requests: agents conflict with\n    // the socket lifecycle (upgrade handoff / stream multiplexing).\n    outgoing.agent = false;\n  } else {\n    // Use default keep-alive agents for connection reuse\n    const targetProto = (options[forward || \"target\"] as URL).protocol ?? \"http\";\n    outgoing.agent = isSSL.test(targetProto) ? defaultAgents.https : defaultAgents.http;\n  }\n  outgoing.localAddress = options.localAddress;\n\n  //\n  // Remark: If we are false and not upgrading, set the connection: close. This is the right thing to do\n  // as node core doesn't handle this COMPLETELY properly yet.\n  //\n  if (!outgoing.agent) {\n    outgoing.headers = outgoing.headers || {};\n    if (\n      typeof outgoing.headers.connection !== \"string\" ||\n      !upgradeHeader.test(outgoing.headers.connection)\n    ) {\n      outgoing.headers.connection = \"close\";\n    }\n  }\n\n  // the final path is target path + relative path requested by user:\n  const target = options[forward || \"target\"];\n  const targetPath = target && options.prependPath !== false ? (target as URL).pathname || \"\" : \"\";\n  const targetSearch =\n    target instanceof URL && options.prependPath !== false ? target.search || \"\" : \"\";\n\n  const reqUrl = req.url || \"\";\n  const qIdx = reqUrl.indexOf(\"?\");\n  const reqPath = qIdx === -1 ? reqUrl : reqUrl.slice(0, qIdx);\n  const reqSearch = qIdx === -1 ? \"\" : reqUrl.slice(qIdx);\n  const normalizedPath = reqPath ? (reqPath[0] === \"/\" ? reqPath : \"/\" + reqPath) : \"/\";\n  let outgoingPath = options.toProxy ? \"/\" + reqUrl : normalizedPath + reqSearch;\n\n  //\n  // Remark: ignorePath will just straight up ignore whatever the request's\n  // path is. This can be labeled as FOOT-GUN material if you do not know what\n  // you are doing and are using conflicting options.\n  //\n  outgoingPath = options.ignorePath ? \"\" : outgoingPath;\n\n  let fullPath = joinURL(targetPath, outgoingPath);\n  // Merge target query string into the outgoing path\n  if (targetSearch) {\n    const hasQuery = fullPath.includes(\"?\");\n    fullPath = hasQuery ? fullPath.replace(\"?\", targetSearch + \"&\") : fullPath + targetSearch;\n  }\n  outgoing.path = fullPath;\n\n  if (options.changeOrigin) {\n    outgoing.headers.host =\n      requiresPort(outgoing.port, (options[forward || \"target\"] as URL).protocol) &&\n      !hasPort(outgoing.host)\n        ? outgoing.host + \":\" + outgoing.port\n        : (outgoing.host ?? undefined);\n  }\n  return outgoing;\n}\n\n// From https://github.com/unjs/h3/blob/e8adfa/src/utils/internal/path.ts#L16C1-L36C2\nexport function joinURL(base: string | undefined, path: string | undefined): string {\n  if (!base || base === \"/\") {\n    return path || \"/\";\n  }\n  if (!path || path === \"/\") {\n    return base || \"/\";\n  }\n  // eslint-disable-next-line unicorn/prefer-at\n  const baseHasTrailing = base[base.length - 1] === \"/\";\n  const pathHasLeading = path[0] === \"/\";\n  if (baseHasTrailing && pathHasLeading) {\n    return base + path.slice(1);\n  }\n  if (!baseHasTrailing && !pathHasLeading) {\n    return base + \"/\" + path;\n  }\n  return base + path;\n}\n\n/**\n * Set the proper configuration for sockets,\n * set no delay and set keep alive, also set\n * the timeout to 0.\n *\n * Examples:\n *\n *    common.setupSocket(socket)\n *    // => Socket\n *\n * @param socket instance to setup\n *\n * @return Return the configured socket.\n *\n * @api private\n */\n\nexport function setupSocket(socket: net.Socket): net.Socket {\n  socket.setTimeout(0);\n  socket.setNoDelay(true);\n\n  socket.setKeepAlive(true, 0);\n\n  return socket;\n}\n\n/**\n * Get the port number from the host. Or guess it based on the connection type.\n *\n * @param req Incoming HTTP request.\n *\n * @return The port number.\n *\n * @api private\n */\nexport function getPort(req: httpNative.IncomingMessage | Http2ServerRequest): string {\n  const hostHeader = (req.headers[\":authority\"] as string | undefined) || req.headers.host;\n  const res = hostHeader ? hostHeader.match(/:(\\d+)/) : \"\";\n  if (res) {\n    return res[1]!;\n  }\n  return hasEncryptedConnection(req) ? \"443\" : \"80\";\n}\n\n/**\n * Check if the request has an encrypted connection.\n *\n * @param req Incoming HTTP request.\n *\n * @return Whether the connection is encrypted or not.\n *\n * @api private\n */\nexport function hasEncryptedConnection(\n  req: httpNative.IncomingMessage | Http2ServerRequest,\n): boolean {\n  const socket = req.socket;\n  return !!socket && \"encrypted\" in socket && socket.encrypted;\n}\n\n/**\n * Rewrites or removes the domain of a cookie header\n *\n * @param header\n * @param config, mapping of domain to rewritten domain.\n *        '*' key to match any domain, null value to remove the domain.\n *\n * @api private\n */\nexport function rewriteCookieProperty(\n  header: string,\n  config: Record<string, string>,\n  property: string,\n): string;\nexport function rewriteCookieProperty(\n  header: string | string[],\n  config: Record<string, string>,\n  property: string,\n): string | string[];\nexport function rewriteCookieProperty(\n  header: string | string[],\n  config: Record<string, string>,\n  property: string,\n): string | string[] {\n  if (Array.isArray(header)) {\n    return header.map(function (headerElement) {\n      return rewriteCookieProperty(headerElement, config, property);\n    });\n  }\n  return header.replace(\n    new RegExp(String.raw`(;\\s*` + property + \"=)([^;]+)\", \"i\"),\n    function (match, prefix, previousValue) {\n      let newValue;\n      if (previousValue in config) {\n        newValue = config[previousValue];\n      } else if (\"*\" in config) {\n        newValue = config[\"*\"];\n      } else {\n        // no match, return previous value\n        return match;\n      }\n      // replace or remove value\n      return newValue ? prefix + newValue : \"\";\n    },\n  );\n}\n\n/**\n * Parse and validate a proxy address.\n *\n * @param addr - URL string (`http://host:port`, `ws://host:port`, `unix:/path`) or a `ProxyAddr` object.\n *\n * @api private\n */\nexport function parseAddr(addr: string | ProxyAddr): ProxyAddr {\n  if (typeof addr === \"string\") {\n    if (addr.startsWith(\"unix:\")) {\n      return { socketPath: addr.slice(5) };\n    }\n    const url = new URL(addr);\n    return {\n      host: url.hostname,\n      port: Number(url.port) || (isSSL.test(url.protocol) ? 443 : 80),\n    };\n  }\n  if (!addr.socketPath && !addr.port) {\n    throw new Error(\"ProxyAddr must have either `port` or `socketPath`\");\n  }\n  return addr;\n}\n\n/**\n * Check the host and see if it potentially has a port in it (keep it simple)\n *\n * @returns Whether we have one or not\n *\n * @api private\n */\nexport function hasPort(host: string | null | undefined): boolean {\n  return host ? !!~host.indexOf(\":\") : false;\n}\n\n/**\n * Check if the port is required for the protocol\n *\n * Ported from https://github.com/unshiftio/requires-port/blob/master/index.js\n *\n * @returns Whether the port is required for the protocol\n *\n * @api private\n */\nexport function requiresPort(_port: string | number, _protocol: string | undefined): boolean {\n  const protocol = _protocol?.split(\":\")[0];\n  const port = +_port;\n\n  if (!port) return false;\n\n  switch (protocol) {\n    case \"http\":\n    case \"ws\": {\n      return port !== 80;\n    }\n\n    case \"https\":\n    case \"wss\": {\n      return port !== 443;\n    }\n\n    case \"ftp\": {\n      return port !== 21;\n    }\n\n    case \"gopher\": {\n      return port !== 70;\n    }\n\n    case \"file\": {\n      return false;\n    }\n  }\n\n  return port !== 0;\n}\n"
  },
  {
    "path": "src/fetch.ts",
    "content": "import type { IncomingMessage, RequestOptions } from \"node:http\";\nimport { request as httpRequest } from \"node:http\";\nimport { request as httpsRequest } from \"node:https\";\nimport { Readable } from \"node:stream\";\nimport type { ProxyAddr } from \"./types.ts\";\nimport { defaultAgents, isSSL, joinURL, parseAddr } from \"./_utils.ts\";\n\n/**\n * Options for {@link proxyFetch}.\n */\nexport interface ProxyFetchOptions {\n  /**\n   * Timeout in milliseconds for the upstream request.\n   * Rejects with an error if the upstream does not respond within this time.\n   */\n  timeout?: number;\n  /**\n   * Add `x-forwarded-for`, `x-forwarded-port`, `x-forwarded-proto`, and\n   * `x-forwarded-host` headers derived from the input URL.\n   * Default: `false`.\n   */\n  xfwd?: boolean;\n  /**\n   * Rewrite the `Host` header to match the target address.\n   * Default: `false` (original host from the input URL is kept).\n   */\n  changeOrigin?: boolean;\n  /**\n   * HTTP agent for connection pooling / reuse.\n   * Default: `false` (no agent, no keep-alive).\n   */\n  agent?: any;\n  /**\n   * Follow HTTP redirects from the upstream.\n   * `true` = max 5 hops; number = custom max.\n   * Default: `false` (manual redirect, raw 3xx responses are returned).\n   */\n  followRedirects?: boolean | number;\n  /**\n   * TLS options forwarded to `https.request` (e.g. `{ rejectUnauthorized: false }`).\n   * Also controls certificate verification — set `rejectUnauthorized: false` to skip.\n   * Default: none.\n   */\n  ssl?: Record<string, unknown>;\n}\n\n/**\n * Proxy a request to a specific server address (TCP host/port or Unix socket)\n * using web standard {@link Request}/{@link Response} interfaces.\n *\n * Supports both HTTP and HTTPS upstream targets.\n *\n * @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.\n * @param input - The request URL (string or URL) or a {@link Request} object.\n * @param inputInit - Optional {@link RequestInit} or {@link Request} to override method, headers, and body.\n * @param opts - Optional proxy options.\n */\nexport async function proxyFetch(\n  addr: string | ProxyAddr,\n  input: string | URL | Request,\n  inputInit?: RequestInit | Request,\n  opts?: ProxyFetchOptions,\n) {\n  const resolvedAddr = parseAddr(addr);\n\n  // Detect protocol and base path from addr string\n  let useHTTPS = false;\n  let addrBasePath = \"\";\n  if (typeof addr === \"string\" && !addr.startsWith(\"unix:\")) {\n    const addrURL = new URL(addr);\n    useHTTPS = isSSL.test(addrURL.protocol);\n    if (addrURL.pathname && addrURL.pathname !== \"/\") {\n      addrBasePath = addrURL.pathname;\n    }\n  }\n\n  let url: URL;\n  let init: RequestInit | undefined;\n\n  if (input instanceof Request) {\n    url = new URL(input.url);\n    init = {\n      ...toInit(input),\n      ...toInit(inputInit),\n    };\n  } else {\n    url = new URL(input);\n    init = toInit(inputInit);\n  }\n  init = {\n    redirect: \"manual\",\n    ...init,\n  };\n  if (init.body) {\n    (init as RequestInit & { duplex: string }).duplex = \"half\";\n  }\n\n  // Merge addr base path with request path\n  const requestPath = url.pathname + url.search;\n  const path = addrBasePath ? joinURL(addrBasePath, requestPath) : requestPath;\n\n  const reqHeaders: Record<string, string | string[]> = {};\n  if (init.headers) {\n    // Fast path: plain object — direct assign, no iteration needed\n    if (!(init.headers instanceof Headers) && !Array.isArray(init.headers)) {\n      Object.assign(reqHeaders, init.headers);\n    } else {\n      // Headers or [key, value][] — both are iterable pairs\n      for (const [key, value] of init.headers as Iterable<[string, string]>) {\n        const existing = reqHeaders[key];\n        if (existing === undefined) {\n          reqHeaders[key] = value;\n        } else {\n          reqHeaders[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];\n        }\n      }\n    }\n  }\n\n  // Add x-forwarded-* headers derived from the input URL\n  if (opts?.xfwd) {\n    if (!reqHeaders[\"x-forwarded-for\"]) {\n      reqHeaders[\"x-forwarded-for\"] = url.hostname;\n    }\n    if (!reqHeaders[\"x-forwarded-port\"]) {\n      reqHeaders[\"x-forwarded-port\"] = url.port || (url.protocol === \"https:\" ? \"443\" : \"80\");\n    }\n    if (!reqHeaders[\"x-forwarded-proto\"]) {\n      reqHeaders[\"x-forwarded-proto\"] = url.protocol.replace(\":\", \"\");\n    }\n    if (!reqHeaders[\"x-forwarded-host\"]) {\n      reqHeaders[\"x-forwarded-host\"] = url.host;\n    }\n  }\n\n  // Rewrite Host header to match the target address\n  if (opts?.changeOrigin) {\n    if (resolvedAddr.socketPath) {\n      reqHeaders.host = \"localhost\";\n    } else {\n      const targetHost = resolvedAddr.host || \"localhost\";\n      const targetPort = resolvedAddr.port;\n      const defaultPort = useHTTPS ? 443 : 80;\n      reqHeaders.host =\n        targetPort && targetPort !== defaultPort ? `${targetHost}:${targetPort}` : targetHost;\n    }\n  }\n\n  const maxRedirects =\n    typeof opts?.followRedirects === \"number\"\n      ? opts.followRedirects\n      : opts?.followRedirects\n        ? 5\n        : 0;\n\n  // Buffer body only when redirects need replay; otherwise stream through\n  const body = maxRedirects > 0 ? await _bufferBody(init.body) : _toNodeStream(init.body);\n\n  // Default to keep-alive agent for connection reuse\n  const agent =\n    opts?.agent !== undefined\n      ? opts.agent || false\n      : useHTTPS\n        ? defaultAgents.https\n        : defaultAgents.http;\n\n  const res = await _sendRequest(\n    useHTTPS ? httpsRequest : httpRequest,\n    init.method || \"GET\",\n    path,\n    reqHeaders,\n    resolvedAddr,\n    body,\n    {\n      signal: init.signal || undefined,\n      agent,\n      timeout: opts?.timeout,\n      ssl: opts?.ssl,\n      maxRedirects,\n      redirectCount: 0,\n      originalHeaders: reqHeaders,\n    },\n  );\n\n  // Build Response — use plain header pairs to avoid Headers object overhead\n  const resHeaders: [string, string][] = [];\n  const rawHeaders = res.rawHeaders;\n  for (let i = 0; i < rawHeaders.length; i += 2) {\n    const key = rawHeaders[i]!;\n    const keyLower = key.toLowerCase();\n    if (\n      keyLower === \"transfer-encoding\" ||\n      keyLower === \"keep-alive\" ||\n      keyLower === \"connection\"\n    ) {\n      continue;\n    }\n    resHeaders.push([key, rawHeaders[i + 1]!]);\n  }\n\n  const hasBody = res.statusCode !== 204 && res.statusCode !== 304;\n  return new Response(hasBody ? (Readable.toWeb(res) as ReadableStream) : null, {\n    status: res.statusCode,\n    statusText: res.statusMessage,\n    headers: resHeaders,\n  });\n}\n\n// --- Internal ---\n\nfunction toInit(init?: RequestInit | Request): RequestInit | undefined {\n  if (!init) {\n    return undefined;\n  }\n  if (init instanceof Request) {\n    return {\n      method: init.method,\n      headers: init.headers,\n      body: init.body,\n      duplex: init.body ? \"half\" : undefined,\n    } as RequestInit;\n  }\n  return init;\n}\n\n/** Convert body to a Node.js Readable or Buffer for streaming without buffering. */\nfunction _toNodeStream(body: BodyInit | null | undefined): Readable | Buffer | undefined {\n  if (!body) {\n    return undefined;\n  }\n  if (typeof body === \"string\") {\n    return Buffer.from(body);\n  }\n  if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {\n    return Buffer.from(body as ArrayBuffer);\n  }\n  if (body instanceof ReadableStream) {\n    return Readable.fromWeb(body as import(\"node:stream/web\").ReadableStream);\n  }\n  if (body instanceof Blob) {\n    return Readable.fromWeb(body.stream() as import(\"node:stream/web\").ReadableStream);\n  }\n  return Buffer.from(String(body));\n}\n\n/** Normalize any body type to Buffer (or undefined) for redirect replay. */\nasync function _bufferBody(body: BodyInit | null | undefined): Promise<Buffer | undefined> {\n  if (!body) {\n    return undefined;\n  }\n  if (typeof body === \"string\") {\n    return Buffer.from(body);\n  }\n  if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {\n    return Buffer.from(body as ArrayBuffer);\n  }\n  if (body instanceof ReadableStream) {\n    const readable = Readable.fromWeb(body as import(\"node:stream/web\").ReadableStream);\n    const chunks: Buffer[] = [];\n    for await (const chunk of readable) {\n      chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n    }\n    return Buffer.concat(chunks);\n  }\n  if (body instanceof Blob) {\n    return Buffer.from(await body.arrayBuffer());\n  }\n  return Buffer.from(String(body));\n}\n\nconst _redirectStatuses = new Set([301, 302, 303, 307, 308]);\n\ninterface _RequestOpts {\n  signal?: AbortSignal;\n  agent?: any;\n  timeout?: number;\n  ssl?: Record<string, unknown>;\n  maxRedirects: number;\n  redirectCount: number;\n  originalHeaders: Record<string, string | string[]>;\n}\n\nfunction _sendRequest(\n  doRequest: typeof httpRequest,\n  method: string,\n  path: string,\n  headers: Record<string, string | string[]>,\n  addr: ProxyAddr,\n  body: Buffer | Readable | undefined,\n  opts: _RequestOpts,\n): Promise<IncomingMessage> {\n  return new Promise<IncomingMessage>((resolve, reject) => {\n    const reqOpts: RequestOptions = {\n      method,\n      path,\n      headers,\n      agent: opts.agent,\n    };\n\n    if (addr.socketPath) {\n      reqOpts.socketPath = addr.socketPath;\n    } else {\n      reqOpts.hostname = addr.host || \"localhost\";\n      reqOpts.port = addr.port;\n    }\n\n    if (opts.signal) {\n      reqOpts.signal = opts.signal;\n    }\n\n    if (opts.ssl) {\n      Object.assign(reqOpts, opts.ssl);\n    }\n\n    const req = doRequest(reqOpts, (res) => {\n      const statusCode = res.statusCode!;\n\n      if (\n        opts.maxRedirects > 0 &&\n        _redirectStatuses.has(statusCode) &&\n        opts.redirectCount < opts.maxRedirects &&\n        res.headers.location\n      ) {\n        res.resume();\n\n        const currentURL = new URL(path, `http://${addr.host || \"localhost\"}:${addr.port || 80}`);\n        const location = new URL(res.headers.location, currentURL);\n        const redirectHTTPS = isSSL.test(location.protocol);\n\n        const preserveMethod = statusCode === 307 || statusCode === 308;\n        const redirectMethod = preserveMethod ? method : \"GET\";\n\n        const redirectHeaders: Record<string, string | string[]> = {\n          ...opts.originalHeaders,\n        };\n        redirectHeaders.host = location.host;\n\n        if (location.host !== currentURL.host) {\n          delete redirectHeaders.authorization;\n          delete redirectHeaders.cookie;\n        }\n\n        if (!preserveMethod) {\n          delete redirectHeaders[\"content-length\"];\n          delete redirectHeaders[\"content-type\"];\n          delete redirectHeaders[\"transfer-encoding\"];\n        }\n\n        _sendRequest(\n          redirectHTTPS ? httpsRequest : httpRequest,\n          redirectMethod,\n          location.pathname + location.search,\n          redirectHeaders,\n          {\n            host: location.hostname,\n            port: Number(location.port) || (redirectHTTPS ? 443 : 80),\n          },\n          preserveMethod ? body : undefined,\n          { ...opts, redirectCount: opts.redirectCount + 1 },\n        ).then(resolve, reject);\n        return;\n      }\n\n      resolve(res);\n    });\n\n    req.on(\"error\", reject);\n\n    if (opts.timeout) {\n      req.setTimeout(opts.timeout, () => {\n        req.destroy(new Error(\"Proxy request timed out\"));\n      });\n    }\n\n    if (body instanceof Readable) {\n      body.on(\"error\", (err) => {\n        req.destroy(err);\n        reject(err);\n      });\n      body.pipe(req);\n    } else if (body) {\n      req.end(body);\n    } else {\n      req.end();\n    }\n  });\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "export type { ProxyAddr, ProxyServerOptions, ProxyTarget, ProxyTargetDetailed } from \"./types.ts\";\nexport { ProxyServer, createProxyServer, type ProxyServerEventMap } from \"./server.ts\";\nexport { proxyFetch, type ProxyFetchOptions } from \"./fetch.ts\";\nexport { proxyUpgrade, type ProxyUpgradeOptions } from \"./ws.ts\";\n"
  },
  {
    "path": "src/middleware/_utils.ts",
    "content": "import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { Socket } from \"node:net\";\nimport type { ProxyServer } from \"../server.ts\";\nimport type { ProxyServerOptions, ProxyTargetDetailed } from \"../types.ts\";\nimport type { Http2ServerRequest, Http2ServerResponse } from \"node:http2\";\n\nexport type ResOfType<T extends \"web\" | \"ws\"> = T extends \"ws\"\n  ? T extends \"web\"\n    ? ServerResponse | Http2ServerResponse | Socket\n    : Socket\n  : T extends \"web\"\n    ? ServerResponse | Http2ServerResponse\n    : never;\n\nexport type ProxyMiddleware<T extends ServerResponse | Http2ServerResponse | Socket> = (\n  req: IncomingMessage | Http2ServerRequest,\n  res: T,\n  opts: ProxyServerOptions & {\n    target: URL | ProxyTargetDetailed;\n    forward: URL;\n  },\n  server: ProxyServer<IncomingMessage | Http2ServerRequest, ServerResponse | Http2ServerResponse>,\n  head?: Buffer,\n  callback?: (err: any, req: IncomingMessage | Http2ServerRequest, socket: T, url?: any) => void,\n) => void | true;\n\nexport function defineProxyMiddleware<T extends ServerResponse | Socket = ServerResponse>(\n  m: ProxyMiddleware<T>,\n) {\n  return m;\n}\n\nexport type ProxyOutgoingMiddleware = (\n  req: IncomingMessage | Http2ServerRequest,\n  res: ServerResponse | Http2ServerResponse,\n  proxyRes: IncomingMessage,\n  opts: ProxyServerOptions & {\n    target: URL | ProxyTargetDetailed;\n    forward: URL;\n  },\n) => void | true;\n\nexport function defineProxyOutgoingMiddleware(m: ProxyOutgoingMiddleware) {\n  return m;\n}\n"
  },
  {
    "path": "src/middleware/web-incoming.ts",
    "content": "import type { ClientRequest, IncomingMessage, ServerResponse } from \"node:http\";\nimport type { ProxyTargetDetailed } from \"../types.ts\";\nimport nodeHTTP from \"node:http\";\nimport nodeHTTPS from \"node:https\";\nimport { getPort, hasEncryptedConnection, isSSL, setupOutgoing } from \"../_utils.ts\";\nimport { webOutgoingMiddleware } from \"./web-outgoing.ts\";\nimport { type ProxyMiddleware, defineProxyMiddleware } from \"./_utils.ts\";\n\nconst nativeAgents = { http: nodeHTTP, https: nodeHTTPS };\nconst redirectStatuses = new Set([301, 302, 303, 307, 308]);\n\n/**\n * Sets `content-length` to '0' if request is of DELETE type.\n */\nexport const deleteLength = defineProxyMiddleware((req) => {\n  if ((req.method === \"DELETE\" || req.method === \"OPTIONS\") && !req.headers[\"content-length\"]) {\n    req.headers[\"content-length\"] = \"0\";\n    delete req.headers[\"transfer-encoding\"];\n  }\n});\n\n/**\n * Sets timeout in request socket if it was specified in options.\n */\nexport const timeout = defineProxyMiddleware((req, res, options) => {\n  if (options.timeout) {\n    req.socket.setTimeout(options.timeout, () => {\n      req.socket.destroy();\n    });\n  }\n});\n\n/**\n * Sets `x-forwarded-*` headers if specified in config.\n */\nexport const XHeaders = defineProxyMiddleware((req, res, options) => {\n  if (!options.xfwd) {\n    return;\n  }\n\n  const encrypted = (req as any).isSpdy || hasEncryptedConnection(req);\n  const values = {\n    for: req.connection.remoteAddress || req.socket.remoteAddress,\n    port: getPort(req),\n    proto: encrypted ? \"https\" : \"http\",\n  };\n\n  for (const header of [\"for\", \"port\", \"proto\"] as const) {\n    const key = \"x-forwarded-\" + header;\n    if (!req.headers[key] && values[header] !== undefined) {\n      req.headers[key] = values[header];\n    }\n  }\n\n  req.headers[\"x-forwarded-host\"] =\n    req.headers[\"x-forwarded-host\"] || req.headers[\":authority\"] || req.headers.host || \"\";\n});\n\n/**\n * Does the actual proxying. If `forward` is enabled fires up\n * a ForwardStream, same happens for ProxyStream. The request\n * just dies otherwise.\n *\n */\nexport const stream = defineProxyMiddleware((req, res, options, server, head, callback) => {\n  // And we begin!\n  server.emit(\"start\", req, res, options.target || options.forward);\n\n  const http = nativeAgents.http;\n  const https = nativeAgents.https;\n\n  const maxRedirects =\n    typeof options.followRedirects === \"number\"\n      ? options.followRedirects\n      : options.followRedirects\n        ? 5\n        : 0;\n\n  if (options.forward) {\n    // If forward enable, so just pipe the request\n    const forwardReq = (isSSL.test(options.forward.protocol || \"http\") ? https : http).request(\n      setupOutgoing(options.ssl || {}, options, req, \"forward\"),\n    );\n\n    // error handler (e.g. ECONNRESET, ECONNREFUSED)\n    // Handle errors on incoming request as well as it makes sense to\n    const forwardError = createErrorHandler(forwardReq, options.forward);\n    req.on(\"error\", forwardError);\n    forwardReq.on(\"error\", forwardError);\n\n    (options.buffer || req).pipe(forwardReq);\n    if (!options.target) {\n      res.end();\n      return;\n    }\n  }\n\n  // Request initalization\n  const proxyReq = (isSSL.test(options.target.protocol || \"http\") ? https : http).request(\n    setupOutgoing(options.ssl || {}, options, req),\n  );\n\n  // Enable developers to modify the proxyReq before headers are sent\n  proxyReq.on(\"socket\", (_socket) => {\n    if (server && !proxyReq.getHeader(\"expect\")) {\n      server.emit(\"proxyReq\", proxyReq, req, res, options);\n    }\n  });\n\n  // allow outgoing socket to timeout so that we could\n  // show an error page at the initial request\n  if (options.proxyTimeout) {\n    proxyReq.setTimeout(options.proxyTimeout, function () {\n      proxyReq.destroy();\n    });\n  }\n\n  // Abort proxy request when client disconnects\n  res.on(\"close\", function () {\n    if (!res.writableFinished) {\n      proxyReq.destroy();\n    }\n  });\n\n  // handle errors in proxy and incoming request, just like for forward proxy\n  const proxyError = createErrorHandler(proxyReq, options.target);\n  req.on(\"error\", proxyError);\n  proxyReq.on(\"error\", proxyError);\n\n  function createErrorHandler(proxyReq: ClientRequest, url: URL | ProxyTargetDetailed) {\n    return function proxyError(err: Error) {\n      if (!req.socket?.writable && (err as NodeJS.ErrnoException).code === \"ECONNRESET\") {\n        server.emit(\"econnreset\", err, req, res, url);\n        return proxyReq.destroy();\n      }\n\n      if (callback) {\n        callback(err, req, res, url);\n      } else {\n        server.emit(\"error\", err, req, res, url);\n      }\n    };\n  }\n\n  // Buffer request body when following redirects (needed for 307/308 replay)\n  let bodyBuffer: Buffer | undefined;\n  if (maxRedirects > 0) {\n    const chunks: Buffer[] = [];\n    const source = options.buffer || req;\n    source.on(\"data\", (chunk: Buffer) => {\n      chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n      proxyReq.write(chunk);\n    });\n    source.on(\"end\", () => {\n      bodyBuffer = Buffer.concat(chunks);\n      proxyReq.end();\n    });\n    source.on(\"error\", (err: Error) => {\n      proxyReq.destroy(err);\n    });\n  } else {\n    proxyReq.on(\"socket\", (socket) => {\n      if (socket.pending) {\n        socket.on(\"connect\", () => (options.buffer || req).pipe(proxyReq));\n      } else {\n        (options.buffer || req).pipe(proxyReq);\n      }\n    });\n  }\n\n  function handleResponse(proxyRes: IncomingMessage, redirectCount: number, currentUrl: URL) {\n    const statusCode = proxyRes.statusCode!;\n\n    if (\n      maxRedirects > 0 &&\n      redirectStatuses.has(statusCode) &&\n      redirectCount < maxRedirects &&\n      proxyRes.headers.location\n    ) {\n      // Drain the redirect response body\n      proxyRes.resume();\n\n      const location = new URL(proxyRes.headers.location, currentUrl);\n\n      // 301/302/303 → GET without body; 307/308 → preserve method and body\n      const preserveMethod = statusCode === 307 || statusCode === 308;\n      const redirectMethod = preserveMethod ? req.method || \"GET\" : \"GET\";\n\n      const isHTTPS = isSSL.test(location.protocol);\n      const agent = isHTTPS ? https : http;\n\n      // Build headers from original request\n      const redirectHeaders: Record<string, string | string[] | undefined> = { ...req.headers };\n      if (options.headers) {\n        Object.assign(redirectHeaders, options.headers);\n      }\n      redirectHeaders.host = location.host;\n\n      // Strip sensitive headers on cross-origin redirects\n      if (location.host !== currentUrl.host) {\n        delete redirectHeaders.authorization;\n        delete redirectHeaders.cookie;\n      }\n\n      // Drop body-related headers when method changes to GET\n      if (!preserveMethod) {\n        delete redirectHeaders[\"content-length\"];\n        delete redirectHeaders[\"content-type\"];\n        delete redirectHeaders[\"transfer-encoding\"];\n      }\n\n      const redirectOpts: nodeHTTP.RequestOptions = {\n        hostname: location.hostname,\n        port: location.port || (isHTTPS ? 443 : 80),\n        path: location.pathname + location.search,\n        method: redirectMethod,\n        headers: redirectHeaders,\n        agent: options.agent || false,\n      };\n\n      if (isHTTPS) {\n        (redirectOpts as nodeHTTPS.RequestOptions).rejectUnauthorized =\n          options.secure === undefined ? true : options.secure;\n      }\n\n      const redirectReq = agent.request(redirectOpts);\n\n      if (server && !redirectReq.getHeader(\"expect\")) {\n        server.emit(\"proxyReq\", redirectReq, req, res, options);\n      }\n\n      if (options.proxyTimeout) {\n        redirectReq.setTimeout(options.proxyTimeout, () => {\n          redirectReq.destroy();\n        });\n      }\n\n      const redirectError = createErrorHandler(redirectReq, location);\n      redirectReq.on(\"error\", redirectError);\n\n      redirectReq.on(\"response\", (nextRes: IncomingMessage) => {\n        handleResponse(nextRes, redirectCount + 1, location);\n      });\n\n      if (preserveMethod && bodyBuffer && bodyBuffer.length > 0) {\n        redirectReq.end(bodyBuffer);\n      } else {\n        redirectReq.end();\n      }\n\n      return;\n    }\n\n    // Non-redirect response (or max redirects exceeded)\n    if (server) {\n      server.emit(\"proxyRes\", proxyRes, req, res);\n    }\n\n    if (!res.headersSent && !options.selfHandleResponse) {\n      for (const pass of webOutgoingMiddleware) {\n        if (pass(req, res, proxyRes, options)) {\n          break;\n        }\n      }\n    }\n\n    if (res.finished) {\n      if (server) {\n        server.emit(\"end\", req, res, proxyRes);\n      }\n    } else {\n      res.on(\"close\", function () {\n        proxyRes.destroy();\n      });\n      proxyRes.on(\"close\", function () {\n        if (!proxyRes.complete && !res.destroyed) {\n          res.destroy();\n        }\n      });\n      proxyRes.on(\"error\", function (err) {\n        if (!res.destroyed) {\n          res.destroy(err);\n        }\n\n        if (server.listenerCount(\"error\") > 0) {\n          server.emit(\"error\", err, req, res, currentUrl);\n        }\n      });\n      proxyRes.on(\"end\", function () {\n        if (server) {\n          server.emit(\"end\", req, res, proxyRes);\n        }\n      });\n      if (!options.selfHandleResponse) {\n        proxyRes.pipe(res);\n      }\n    }\n  }\n\n  proxyReq.on(\"response\", function (proxyRes) {\n    handleResponse(proxyRes, 0, options.target as URL);\n  });\n});\n\nexport const webIncomingMiddleware: readonly ProxyMiddleware<ServerResponse>[] = [\n  deleteLength,\n  timeout,\n  XHeaders,\n  stream,\n] as const;\n"
  },
  {
    "path": "src/middleware/web-outgoing.ts",
    "content": "import { rewriteCookieProperty } from \"../_utils.ts\";\nimport type { ProxyTarget, ProxyTargetDetailed } from \"../types.ts\";\nimport { type ProxyOutgoingMiddleware, defineProxyOutgoingMiddleware } from \"./_utils.ts\";\n\nconst redirectRegex = /^201|30([12378])$/;\n\n/**\n * Remove chunked transfer-encoding for HTTP/1.0, HTTP/2, and bodyless (204/304) responses\n */\nexport const removeChunked = defineProxyOutgoingMiddleware((req, res, proxyRes) => {\n  // HTTP/1.0 and HTTP/2 do not have transfer-encoding: chunked\n  // 204 and 304 responses MUST NOT contain a message body (RFC 9110)\n  if (\n    req.httpVersion === \"1.0\" ||\n    req.httpVersionMajor >= 2 ||\n    proxyRes.statusCode === 204 ||\n    proxyRes.statusCode === 304\n  ) {\n    delete proxyRes.headers[\"transfer-encoding\"];\n  }\n});\n\n/**\n * If is a HTTP 1.0 request, set the correct connection header\n * or if connection header not present, then use `keep-alive`\n *\n * If is a HTTP/2 request, remove connection header no matter what,\n * this avoids sending connection header to the underlying http2 client\n */\nexport const setConnection = defineProxyOutgoingMiddleware((req, res, proxyRes) => {\n  if (req.httpVersion === \"1.0\") {\n    proxyRes.headers.connection = req.headers.connection || \"close\";\n  } else if (req.httpVersionMajor < 2 && !proxyRes.headers.connection) {\n    proxyRes.headers.connection = req.headers.connection || \"keep-alive\";\n  } else if (req.httpVersionMajor >= 2) {\n    delete proxyRes.headers.connection;\n  }\n});\n\nexport const setRedirectHostRewrite = defineProxyOutgoingMiddleware(\n  (req, res, proxyRes, options) => {\n    if (\n      (options.hostRewrite || options.autoRewrite || options.protocolRewrite) &&\n      proxyRes.headers.location &&\n      redirectRegex.test(String(proxyRes.statusCode))\n    ) {\n      const target = _toURL(options.target!);\n      const u = new URL(proxyRes.headers.location, target);\n\n      // Make sure the redirected host matches the target host before rewriting\n      if (target.host !== u.host) {\n        return;\n      }\n\n      if (options.hostRewrite) {\n        u.host = options.hostRewrite;\n      } else if (options.autoRewrite) {\n        if (req.headers[\":authority\"]) {\n          u.host = req.headers[\":authority\"] as string;\n        } else if (req.headers.host) {\n          u.host = req.headers.host;\n        }\n      }\n      if (options.protocolRewrite) {\n        u.protocol = options.protocolRewrite;\n      }\n\n      proxyRes.headers.location = u.toString();\n    }\n  },\n);\n\n/**\n * Copy headers from proxyResponse to response\n * set each header in response object.\n *\n * @param {ClientRequest} Req Request object\n * @param {IncomingMessage} Res Response object\n * @param {proxyResponse} Res Response object from the proxy request\n * @param {Object} Options options.cookieDomainRewrite: Config to rewrite cookie domain\n *\n * @api private\n */\nexport const writeHeaders = defineProxyOutgoingMiddleware((req, res, proxyRes, options) => {\n  const rewriteCookieDomainConfig =\n    typeof options.cookieDomainRewrite === \"string\"\n      ? // also test for ''\n        { \"*\": options.cookieDomainRewrite }\n      : options.cookieDomainRewrite;\n  const rewriteCookiePathConfig =\n    typeof options.cookiePathRewrite === \"string\"\n      ? // also test for ''\n        { \"*\": options.cookiePathRewrite }\n      : options.cookiePathRewrite;\n\n  const preserveHeaderKeyCase = options.preserveHeaderKeyCase;\n  let rawHeaderKeyMap: Record<string, string> | undefined;\n  const setHeader = function (key: string, header: string | string[] | undefined) {\n    if (header === undefined || !String(key).trim()) {\n      return;\n    }\n    if (rewriteCookieDomainConfig && key.toLowerCase() === \"set-cookie\") {\n      header = rewriteCookieProperty(header, rewriteCookieDomainConfig, \"domain\");\n    }\n    if (rewriteCookiePathConfig && key.toLowerCase() === \"set-cookie\") {\n      header = rewriteCookieProperty(header, rewriteCookiePathConfig, \"path\");\n    }\n    try {\n      res.setHeader(String(key).trim(), header);\n    } catch {\n      // Skip headers with invalid characters (e.g. control chars)\n      // to avoid crashing with ERR_INVALID_CHAR\n    }\n  };\n\n  // message.rawHeaders is added in: v0.11.6\n  // https://nodejs.org/api/http.html#http_message_rawheaders\n  if (preserveHeaderKeyCase && proxyRes.rawHeaders !== undefined) {\n    rawHeaderKeyMap = {};\n    for (let i = 0; i < proxyRes.rawHeaders.length; i += 2) {\n      const key = proxyRes.rawHeaders[i]!;\n      rawHeaderKeyMap[key.toLowerCase()] = key;\n    }\n  }\n\n  for (let key of Object.keys(proxyRes.headers)) {\n    const header = proxyRes.headers[key];\n    if (preserveHeaderKeyCase && rawHeaderKeyMap) {\n      key = rawHeaderKeyMap[key] || key;\n    }\n    setHeader(key, header);\n  }\n});\n\n/**\n * Set the statusCode from the proxyResponse\n */\nexport const writeStatusCode = defineProxyOutgoingMiddleware((req, res, proxyRes) => {\n  // From Node.js docs: response.writeHead(statusCode[, statusMessage][, headers])\n\n  // @ts-expect-error\n  res.statusCode = proxyRes.statusCode;\n\n  if (\n    proxyRes.statusMessage &&\n    // Only HTTP/1.0 and HTTP/1.1 support statusMessage\n    req.httpVersionMajor < 2\n  ) {\n    res.statusMessage = proxyRes.statusMessage;\n  }\n});\n\nexport const webOutgoingMiddleware: readonly ProxyOutgoingMiddleware[] = [\n  removeChunked,\n  setConnection,\n  setRedirectHostRewrite,\n  writeHeaders,\n  writeStatusCode,\n] as const;\n\n// --- Internal ---\n\nfunction _toURL(target: ProxyTarget): URL {\n  if (target instanceof URL) {\n    return target;\n  }\n  if (typeof target === \"string\") {\n    return new URL(target);\n  }\n  const protocol = (target as ProxyTargetDetailed).protocol || \"http:\";\n  const host =\n    (target as ProxyTargetDetailed).host || (target as ProxyTargetDetailed).hostname || \"localhost\";\n  const port = (target as ProxyTargetDetailed).port;\n  return new URL(`${protocol}//${host}${port ? \":\" + port : \"\"}`);\n}\n"
  },
  {
    "path": "src/middleware/ws-incoming.ts",
    "content": "import nodeHTTP from \"node:http\";\nimport nodeHTTPS from \"node:https\";\nimport type { Socket } from \"node:net\";\nimport { type ProxyMiddleware, defineProxyMiddleware } from \"./_utils.ts\";\nimport { getPort, hasEncryptedConnection, isSSL, setupOutgoing, setupSocket } from \"../_utils.ts\";\n\n/**\n * WebSocket requests must have the `GET` method and\n * the `upgrade:websocket` header\n */\nexport const checkMethodAndHeader = defineProxyMiddleware<Socket>((req, socket) => {\n  if (req.method !== \"GET\" || !req.headers.upgrade) {\n    socket.destroy();\n    return true;\n  }\n\n  if (req.headers.upgrade.toLowerCase() !== \"websocket\") {\n    socket.destroy();\n    return true;\n  }\n});\n\n/**\n * Sets `x-forwarded-*` headers if specified in config.\n */\nexport const XHeaders = defineProxyMiddleware<Socket>((req, socket, options) => {\n  if (!options.xfwd) {\n    return;\n  }\n\n  const values = {\n    for: req.connection.remoteAddress || req.socket.remoteAddress,\n    port: getPort(req),\n    proto: hasEncryptedConnection(req) ? \"wss\" : \"ws\",\n  };\n\n  for (const header of [\"for\", \"port\", \"proto\"] as const) {\n    const key = \"x-forwarded-\" + header;\n    if (!req.headers[key] && values[header] !== undefined) {\n      req.headers[key] = values[header];\n    }\n  }\n});\n\n/**\n * Does the actual proxying. Make the request and upgrade it\n * send the Switching Protocols request and pipe the sockets.\n */\nexport const stream = defineProxyMiddleware<Socket>(\n  (req, socket, options, server, head, callback) => {\n    const createHttpHeader = function (line: string, headers: nodeHTTP.OutgoingHttpHeaders) {\n      return (\n        Object.keys(headers)\n          // eslint-disable-next-line unicorn/no-array-reduce\n          .reduce(\n            function (head, key) {\n              const value = headers[key];\n\n              if (!Array.isArray(value)) {\n                head.push(key + \": \" + value);\n                return head;\n              }\n\n              for (const element of value) {\n                head.push(key + \": \" + element);\n              }\n              return head;\n            },\n            [line],\n          )\n          .join(\"\\r\\n\") + \"\\r\\n\\r\\n\"\n      );\n    };\n\n    setupSocket(socket);\n\n    if (head && head.length > 0) {\n      socket.unshift(head);\n    }\n\n    // Attach error handler early so client socket errors (e.g. ECONNRESET\n    // from an intermediary proxy timeout) are caught before the upstream\n    // upgrade response arrives. (#79)\n    socket.on(\"error\", onSocketError);\n\n    const proxyReq = (isSSL.test(options.target.protocol || \"http\") ? nodeHTTPS : nodeHTTP).request(\n      setupOutgoing(options.ssl || {}, options, req),\n    );\n\n    // Enable developers to modify the proxyReq before headers are sent\n    if (server) {\n      server.emit(\"proxyReqWs\", proxyReq, req, socket, options, head);\n    }\n\n    // Error Handler\n    proxyReq.on(\"error\", onOutgoingError);\n    proxyReq.on(\"response\", function (res) {\n      // if upgrade event isn't going to happen, close the socket\n      // guard against writing to an already-destroyed socket\n      // (https://github.com/http-party/node-http-proxy/pull/1433)\n      if (!(res as any).upgrade) {\n        if (!socket.destroyed && socket.writable) {\n          socket.write(\n            createHttpHeader(\n              \"HTTP/\" + res.httpVersion + \" \" + res.statusCode + \" \" + res.statusMessage,\n              res.headers,\n            ),\n          );\n          res.on(\"error\", onOutgoingError);\n          res.pipe(socket);\n        } else {\n          // Socket already gone — consume response to avoid unhandled stream errors\n          res.resume();\n        }\n      }\n    });\n\n    proxyReq.on(\"upgrade\", function (proxyRes, proxySocket, proxyHead) {\n      proxySocket.on(\"error\", onOutgoingError);\n\n      // Allow us to listen when the websocket has completed\n      proxySocket.on(\"end\", function () {\n        server.emit(\"close\", proxyRes, proxySocket, proxyHead);\n      });\n\n      // Remove the pre-upgrade error handler — it calls proxyReq.destroy()\n      // which is pointless after upgrade and emits a spurious error event.\n      socket.removeListener(\"error\", onSocketError);\n\n      // The pipe below will end proxySocket if socket closes cleanly, but not\n      // if it errors (eg, vanishes from the net and starts returning\n      // EHOSTUNREACH). We need to do that explicitly.\n      socket.on(\"error\", function () {\n        proxySocket.end();\n      });\n\n      setupSocket(proxySocket);\n\n      if (proxyHead && proxyHead.length > 0) {\n        proxySocket.unshift(proxyHead);\n      }\n\n      //\n      // Remark: Handle writing the headers to the socket when switching protocols\n      // Also handles when a header is an array\n      //\n      socket.write(createHttpHeader(\"HTTP/1.1 101 Switching Protocols\", proxyRes.headers));\n\n      proxySocket.pipe(socket).pipe(proxySocket);\n\n      server.emit(\"open\", proxySocket);\n      server.emit(\"proxySocket\", proxySocket); // DEPRECATED.\n    });\n\n    proxyReq.end(); // XXX: CHECK IF THIS IS THIS CORRECT\n    // return;\n\n    function onSocketError(err: Error) {\n      if (callback) {\n        callback(err, req, socket);\n      } else {\n        server.emit(\"error\", err, req, socket);\n      }\n      proxyReq.destroy();\n    }\n\n    function onOutgoingError(err: Error) {\n      if (callback) {\n        callback(err, req, socket);\n      } else {\n        server.emit(\"error\", err, req, socket);\n      }\n      socket.end();\n    }\n  },\n);\n\nexport const websocketIncomingMiddleware: readonly ProxyMiddleware<Socket>[] = [\n  checkMethodAndHeader,\n  XHeaders,\n  stream,\n] as const;\n"
  },
  {
    "path": "src/server.ts",
    "content": "import http from \"node:http\";\nimport https from \"node:https\";\nimport http2 from \"node:http2\";\nimport { EventEmitter } from \"node:events\";\nimport { webIncomingMiddleware } from \"./middleware/web-incoming.ts\";\nimport { websocketIncomingMiddleware } from \"./middleware/ws-incoming.ts\";\nimport type { ProxyServerOptions, ProxyTarget } from \"./types.ts\";\nimport type { ProxyMiddleware, ResOfType } from \"./middleware/_utils.ts\";\nimport type net from \"node:net\";\n\nexport interface ProxyServerEventMap<\n  Req extends http.IncomingMessage | http2.Http2ServerRequest = http.IncomingMessage,\n  Res extends http.ServerResponse | http2.Http2ServerResponse = http.ServerResponse,\n> {\n  error: [err: Error, req?: Req, res?: Res | net.Socket, target?: URL | ProxyTarget];\n  start: [req: Req, res: Res, target: URL | ProxyTarget];\n  econnreset: [err: Error, req: Req, res: Res, target: URL | ProxyTarget];\n  proxyReq: [proxyReq: http.ClientRequest, req: Req, res: Res, options: ProxyServerOptions];\n  proxyReqWs: [\n    proxyReq: http.ClientRequest,\n    req: Req,\n    socket: net.Socket,\n    options: ProxyServerOptions,\n    head: any,\n  ];\n  proxyRes: [proxyRes: http.IncomingMessage, req: Req, res: Res];\n  end: [req: Req, res: Res, proxyRes: http.IncomingMessage];\n  open: [proxySocket: net.Socket];\n  /** @deprecated */\n  proxySocket: [proxySocket: net.Socket];\n  close: [proxyRes: Req, proxySocket: net.Socket, proxyHead: any];\n}\n\n// eslint-disable-next-line unicorn/prefer-event-target\nexport class ProxyServer<\n  Req extends http.IncomingMessage | http2.Http2ServerRequest = http.IncomingMessage,\n  Res extends http.ServerResponse | http2.Http2ServerResponse = http.ServerResponse,\n> extends EventEmitter<ProxyServerEventMap<Req, Res>> {\n  // we use http2.Http2Server to handle HTTP/1.1 HTTPS as well (with allowHTTP1 enabled)\n  private _server?: http.Server | https.Server | http2.Http2SecureServer;\n\n  _webPasses: ProxyMiddleware<http.ServerResponse>[] = [...webIncomingMiddleware];\n  _wsPasses: ProxyMiddleware<net.Socket>[] = [...websocketIncomingMiddleware];\n\n  options: ProxyServerOptions;\n\n  web: (req: Req, res: Res, opts?: ProxyServerOptions, head?: any) => Promise<void>;\n\n  ws: (req: Req, socket: net.Socket, opts: ProxyServerOptions, head?: any) => Promise<void>;\n\n  /**\n   * Creates the proxy server with specified options.\n   * @param options - Config object passed to the proxy\n   */\n  constructor(options: ProxyServerOptions = {}) {\n    super();\n\n    this.options = options || {};\n    this.options.prependPath = options.prependPath !== false;\n\n    this.web = _createProxyFn(\"web\", this);\n    this.ws = _createProxyFn(\"ws\", this);\n  }\n\n  /**\n   * A function that wraps the object in a webserver, for your convenience\n   * @param port - Port to listen on\n   * @param hostname - The hostname to listen on\n   * @param listeningListener - A callback function that is called when the server starts listening\n   */\n  listen(port: number, hostname?: string, listeningListener?: () => void) {\n    const closure = (\n      req: http.IncomingMessage | http2.Http2ServerRequest,\n      res: http.ServerResponse | http2.Http2ServerResponse,\n    ) => {\n      return this.web(req as Req, res as Res);\n    };\n\n    if (this.options.http2) {\n      if (!this.options.ssl) {\n        throw new Error(\"HTTP/2 requires ssl option\");\n      }\n      this._server = http2.createSecureServer({ ...this.options.ssl, allowHTTP1: true }, closure);\n    } else if (this.options.ssl) {\n      this._server = https.createServer(this.options.ssl, closure);\n    } else {\n      this._server = http.createServer(closure);\n    }\n\n    if (this.options.ws) {\n      this._server.on(\"upgrade\", (req, socket, head) => {\n        this.ws(req, socket, this.options, head).catch(() => {});\n      });\n    }\n\n    this._server.listen(port, hostname, listeningListener);\n\n    return this;\n  }\n\n  /**\n   * A function that closes the inner webserver and stops listening on given port\n   */\n  close(callback?: () => void) {\n    if (this._server) {\n      // Wrap callback to nullify server after all open connections are closed.\n      this._server.close((...args) => {\n        this._server = undefined;\n        if (callback) {\n          Reflect.apply(callback, undefined, args);\n        }\n      });\n    }\n  }\n\n  before<Type extends \"ws\" | \"web\">(\n    type: Type,\n    passName: string,\n    pass: ProxyMiddleware<ResOfType<Type>>,\n  ) {\n    if (type !== \"ws\" && type !== \"web\") {\n      throw new Error(\"type must be `web` or `ws`\");\n    }\n    const passes = this._getPasses(type);\n    let i: false | number = false;\n    for (const [idx, v] of passes.entries()) {\n      if (v.name === passName) {\n        i = idx;\n      }\n    }\n    if (i === false) {\n      throw new Error(\"No such pass\");\n    }\n    passes.splice(i, 0, pass);\n  }\n\n  after<Type extends \"ws\" | \"web\">(\n    type: Type,\n    passName: string,\n    pass: ProxyMiddleware<ResOfType<Type>>,\n  ) {\n    if (type !== \"ws\" && type !== \"web\") {\n      throw new Error(\"type must be `web` or `ws`\");\n    }\n    const passes = this._getPasses(type);\n    let i: boolean | number = false;\n    for (const [idx, v] of passes.entries()) {\n      if (v.name === passName) {\n        i = idx;\n      }\n    }\n    if (i === false) {\n      throw new Error(\"No such pass\");\n    }\n    passes.splice(i++, 0, pass);\n  }\n\n  /** @internal */\n  _getPasses<Type extends \"ws\" | \"web\">(type: Type): ProxyMiddleware<ResOfType<Type>>[] {\n    return (type === \"ws\" ? this._wsPasses : this._webPasses) as unknown as ProxyMiddleware<\n      ResOfType<Type>\n    >[];\n  }\n}\n\n/**\n * Creates the proxy server.\n *\n * Examples:\n *\n *    httpProxy.createProxyServer({ .. }, 8000)\n *    // => '{ web: [Function], ws: [Function] ... }'\n *\n * @param {Object} Options Config object passed to the proxy\n *\n * @return {Object} Proxy Proxy object with handlers for `ws` and `web` requests\n *\n * @api public\n */\nexport function createProxyServer(options: ProxyServerOptions = {}) {\n  return new ProxyServer(options);\n}\n\n// --- Internal ---\n\nfunction _createProxyFn<\n  Type extends \"web\" | \"ws\",\n  ProxyServerReq extends http.IncomingMessage | http2.Http2ServerRequest,\n  ProxyServerRes extends http.ServerResponse | http2.Http2ServerResponse,\n>(type: Type, server: ProxyServer<ProxyServerReq, ProxyServerRes>) {\n  return function (\n    this: ProxyServer<ProxyServerReq, ProxyServerRes>,\n    req: ProxyServerReq,\n    res: ResOfType<Type>,\n    opts?: ProxyServerOptions,\n    head?: any,\n  ): Promise<void> {\n    const requestOptions = { ...opts, ...server.options };\n\n    for (const key of [\"target\", \"forward\"] as const) {\n      if (typeof requestOptions[key] === \"string\") {\n        requestOptions[key] = new URL(requestOptions[key]);\n      }\n    }\n\n    if (!requestOptions.target && !requestOptions.forward) {\n      this.emit(\"error\", new Error(\"Must provide a proper URL as target\"));\n      return Promise.resolve();\n    }\n\n    let _resolve!: () => void;\n    let _reject!: (error: any) => void;\n    const callbackPromise = new Promise<void>((resolve, reject) => {\n      _resolve = resolve;\n      _reject = reject;\n    });\n\n    res.on(\"close\", () => {\n      _resolve();\n    });\n    res.on(\"error\", (error: any) => {\n      _reject(error);\n    });\n\n    for (const pass of server._getPasses(type)) {\n      let stop: void | true;\n      try {\n        stop = pass(\n          req,\n          res,\n          requestOptions as ProxyServerOptions & { target: URL; forward: URL },\n          server as ProxyServer<\n            http.IncomingMessage | http2.Http2ServerRequest,\n            http.ServerResponse | http2.Http2ServerResponse\n          >,\n          head,\n          (error) => {\n            if (server.listenerCount(\"error\") > 0) {\n              server.emit(\"error\", error, req, res as ProxyServerRes | net.Socket);\n              _resolve();\n            } else {\n              _reject(error);\n            }\n          },\n        );\n      } catch (error) {\n        if (server.listenerCount(\"error\") > 0) {\n          server.emit(\"error\", error as Error, req, res as ProxyServerRes | net.Socket);\n          _resolve();\n        } else {\n          _reject(error);\n        }\n        break;\n      }\n      // Passes can return a truthy value to halt the loop\n      if (stop) {\n        _resolve();\n        break;\n      }\n    }\n\n    return callbackPromise;\n  };\n}\n"
  },
  {
    "path": "src/types.ts",
    "content": "import type * as stream from \"node:stream\";\n\nexport interface ProxyTargetDetailed {\n  host?: string;\n  port?: number | string;\n  protocol?: string;\n  hostname?: string;\n  socketPath?: string;\n  key?: string;\n  passphrase?: string;\n  pfx?: Buffer | string;\n  cert?: string;\n  ca?: string;\n  ciphers?: string;\n  secureProtocol?: string;\n}\n\nexport type ProxyTarget = string | URL | ProxyTargetDetailed;\n\n/** Resolved proxy address — either TCP (host + port) or Unix socket. */\nexport type ProxyAddr =\n  | { host?: string; port: number; socketPath?: undefined }\n  | { host?: undefined; port?: undefined; socketPath: string };\n\nexport interface ProxyServerOptions {\n  /** URL string to be parsed. */\n  target?: ProxyTarget;\n  /** URL string to be parsed. */\n  forward?: ProxyTarget;\n  /** Object to be passed to http(s).request. */\n  agent?: any;\n  /** Enable HTTP/2 listener, default is `false` */\n  http2?: boolean;\n  /** Object to be passed to https.createServer()\n   * or http2.createSecureServer() if the `http2` option is enabled\n   */\n  ssl?: any;\n  /** If you want to proxy websockets. */\n  ws?: boolean;\n  /** Adds x- forward headers. */\n  xfwd?: boolean;\n  /** Verify SSL certificate. */\n  secure?: boolean;\n  /** Explicitly specify if we are proxying to another proxy. */\n  toProxy?: boolean;\n  /** Specify whether you want to prepend the target's path to the proxy path. */\n  prependPath?: boolean;\n  /** Specify whether you want to ignore the proxy path of the incoming request. */\n  ignorePath?: boolean;\n  /** Local interface string to bind for outgoing connections. */\n  localAddress?: string;\n  /** Changes the origin of the host header to the target URL. */\n  changeOrigin?: boolean;\n  /** specify whether you want to keep letter case of response header key */\n  preserveHeaderKeyCase?: boolean;\n  /** Basic authentication i.e. 'user:password' to compute an Authorization header. */\n  auth?: string;\n  /** Rewrites the location hostname on (301 / 302 / 307 / 308) redirects, Default: null. */\n  hostRewrite?: string;\n  /** Rewrites the location host/ port on (301 / 302 / 307 / 308) redirects based on requested host/ port.Default: false. */\n  autoRewrite?: boolean;\n  /** Rewrites the location protocol on (301 / 302 / 307 / 308) redirects to 'http' or 'https'.Default: null. */\n  protocolRewrite?: string;\n  /** Rewrites domain of set-cookie headers. */\n  cookieDomainRewrite?: false | string | { [oldDomain: string]: string };\n  /** Rewrites path of set-cookie headers. Default: false */\n  cookiePathRewrite?: false | string | { [oldPath: string]: string };\n  /** Object with extra headers to be added to target requests. */\n  headers?: { [header: string]: string };\n  /** Timeout (in milliseconds) when proxy receives no response from target. Default: 120000 (2 minutes) */\n  proxyTimeout?: number;\n  /** Timeout (in milliseconds) for incoming requests */\n  timeout?: number;\n  /** 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 */\n  selfHandleResponse?: boolean;\n  /** Follow HTTP redirects from target. `true` = max 5 hops; number = custom max. */\n  followRedirects?: boolean | number;\n  /** Buffer */\n  buffer?: stream.Stream;\n}\n"
  },
  {
    "path": "src/ws.ts",
    "content": "import type { IncomingMessage, RequestOptions } from \"node:http\";\nimport { request as httpRequest } from \"node:http\";\nimport { request as httpsRequest } from \"node:https\";\nimport type { Duplex } from \"node:stream\";\nimport type { Socket } from \"node:net\";\nimport type { ProxyAddr } from \"./types.ts\";\nimport {\n  getPort,\n  hasEncryptedConnection,\n  isSSL,\n  parseAddr,\n  setupOutgoing,\n  setupSocket,\n} from \"./_utils.ts\";\n\n/**\n * Options for {@link proxyUpgrade}.\n */\nexport interface ProxyUpgradeOptions {\n  /**\n   * Add `x-forwarded-for`, `x-forwarded-port`, and `x-forwarded-proto` headers.\n   * Default: `true`.\n   */\n  xfwd?: boolean;\n  /**\n   * Rewrite the `Host` header to match the target.\n   * Default: `false` (original host is kept).\n   */\n  changeOrigin?: boolean;\n  /**\n   * Extra headers to include in the upstream upgrade request.\n   * Default: none.\n   */\n  headers?: Record<string, string>;\n  /**\n   * TLS options forwarded to `https.request`.\n   * Default: none.\n   */\n  ssl?: Record<string, unknown>;\n  /**\n   * Whether to verify upstream TLS certificates.\n   * Default: `true`.\n   */\n  secure?: boolean;\n  /**\n   * HTTP/HTTPS agent used for the upstream request.\n   * Default: `false` (no keep-alive agent is used).\n   */\n  agent?: any;\n  /**\n   * Local interface address to bind for upstream connections.\n   * Default: OS-selected local address.\n   */\n  localAddress?: string;\n  /**\n   * Basic auth credentials in `username:password` format.\n   * Default: none.\n   */\n  auth?: string;\n  /**\n   * Prepend the target path to the proxied request path.\n   * Default: `true`.\n   */\n  prependPath?: boolean;\n  /**\n   * Ignore the incoming request path when building the upstream path.\n   * Default: `false` (incoming path is used).\n   */\n  ignorePath?: boolean;\n  /**\n   * Send absolute URL in request path when proxying to another proxy.\n   * Default: `false` (path-only request target is used).\n   */\n  toProxy?: boolean;\n}\n\n/**\n * Proxy a WebSocket upgrade request to a target address without creating a\n * {@link ProxyServer} instance. Similar to {@link proxyFetch} but for\n * WebSocket upgrades.\n *\n * @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.\n * @param req - The incoming HTTP upgrade request.\n * @param socket - The network socket between the server and client.\n * @param head - The first packet of the upgraded stream (may be empty).\n * @param opts - Optional proxy options.\n * @returns A promise that resolves with the upstream proxy socket once the\n * WebSocket connection is established, or rejects on error.\n */\nexport function proxyUpgrade(\n  addr: string | ProxyAddr,\n  req: IncomingMessage,\n  socket: Duplex,\n  head?: Buffer,\n  opts?: ProxyUpgradeOptions,\n): Promise<Socket> {\n  const resolvedAddr = parseAddr(addr);\n\n  // Detect SSL from addr string protocol (wss:// or https://)\n  let useSSL = false;\n  if (typeof addr === \"string\" && !addr.startsWith(\"unix:\")) {\n    useSSL = isSSL.test(new URL(addr).protocol);\n  }\n\n  // Validate WS upgrade request\n  if (req.method !== \"GET\" || req.headers.upgrade?.toLowerCase() !== \"websocket\") {\n    socket.destroy();\n    return Promise.reject(new Error(\"Not a valid WebSocket upgrade request\"));\n  }\n\n  // Set x-forwarded-* headers (enabled by default)\n  if (opts?.xfwd !== false) {\n    const xfFor = req.headers[\"x-forwarded-for\"];\n    const xfPort = req.headers[\"x-forwarded-port\"];\n    const xfProto = req.headers[\"x-forwarded-proto\"];\n    req.headers[\"x-forwarded-for\"] = `${xfFor ? `${xfFor},` : \"\"}${req.socket?.remoteAddress}`;\n    req.headers[\"x-forwarded-port\"] = `${xfPort ? `${xfPort},` : \"\"}${getPort(req)}`;\n    req.headers[\"x-forwarded-proto\"] =\n      `${xfProto ? `${xfProto},` : \"\"}${hasEncryptedConnection(req) ? \"wss\" : \"ws\"}`;\n  }\n\n  // Build target URL for setupOutgoing\n  const target = _buildTargetURL(resolvedAddr, useSSL);\n  const requestOptions: ProxyUpgradeOptions & { target: URL } = {\n    ...opts,\n    target,\n    prependPath: opts?.prependPath !== false,\n  };\n\n  const outgoing = setupOutgoing(\n    requestOptions.ssl || {},\n    requestOptions as Parameters<typeof setupOutgoing>[1],\n    req,\n  );\n\n  const sock = socket as Socket;\n\n  return new Promise<Socket>((resolve, reject) => {\n    let settled = false;\n\n    setupSocket(sock);\n\n    if (head && head.length > 0) {\n      sock.unshift(head);\n    }\n\n    sock.once(\"error\", onSocketError);\n\n    const doRequest = isSSL.test(target.protocol) ? httpsRequest : httpRequest;\n    const proxyReq = doRequest(outgoing as RequestOptions);\n\n    proxyReq.once(\"error\", onOutgoingError);\n\n    proxyReq.once(\"response\", (res) => {\n      // If upgrade event isn't going to happen, relay the response and reject\n      // Guard against writing to an already-destroyed socket\n      // (https://github.com/http-party/node-http-proxy/pull/1433)\n      if (!(res as any).upgrade) {\n        if (!sock.destroyed && sock.writable) {\n          sock.write(\n            _createHttpHeader(\n              `HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}`,\n              res.headers,\n            ),\n          );\n          res.on(\"error\", onOutgoingError);\n          res.pipe(sock);\n        } else {\n          // Socket already gone — consume response to avoid unhandled stream errors\n          res.resume();\n        }\n        if (!settled) {\n          settled = true;\n          reject(new Error(\"Upstream server did not upgrade the connection\"));\n        }\n      }\n    });\n\n    proxyReq.once(\"upgrade\", (proxyRes, proxySocket, proxyHead) => {\n      proxySocket.once(\"error\", onOutgoingError);\n\n      sock.removeListener(\"error\", onSocketError);\n      sock.once(\"error\", () => {\n        proxySocket.end();\n      });\n\n      setupSocket(proxySocket);\n\n      if (proxyHead && proxyHead.length > 0) {\n        proxySocket.unshift(proxyHead);\n      }\n\n      sock.write(_createHttpHeader(\"HTTP/1.1 101 Switching Protocols\", proxyRes.headers));\n      proxySocket.pipe(sock).pipe(proxySocket);\n\n      settled = true;\n      resolve(proxySocket);\n    });\n\n    proxyReq.end();\n\n    function onSocketError(err: Error) {\n      proxyReq.destroy();\n      if (!settled) {\n        settled = true;\n        reject(err);\n      }\n    }\n\n    function onOutgoingError(err: Error) {\n      sock.end();\n      if (!settled) {\n        settled = true;\n        reject(err);\n      }\n    }\n  });\n}\n\n// --- Internal ---\n\nfunction _buildTargetURL(addr: ProxyAddr, useSSL = false): URL {\n  const protocol = useSSL ? \"https\" : \"http\";\n  if (addr.socketPath) {\n    const url = new URL(`${protocol}://unix`);\n    (url as any).socketPath = addr.socketPath;\n    return url;\n  }\n  return new URL(`${protocol}://${addr.host || \"localhost\"}${addr.port ? `:${addr.port}` : \"\"}`);\n}\n\nfunction _createHttpHeader(\n  line: string,\n  headers: Record<string, string | string[] | undefined>,\n): string {\n  let result = line;\n  for (const key of Object.keys(headers)) {\n    const value = headers[key];\n    if (value === undefined) {\n      continue;\n    }\n    if (Array.isArray(value)) {\n      for (const element of value) {\n        result += `\\r\\n${key}: ${element}`;\n      }\n    } else {\n      result += `\\r\\n${key}: ${value}`;\n    }\n  }\n  return `${result}\\r\\n\\r\\n`;\n}\n"
  },
  {
    "path": "test/_stubs.ts",
    "content": "import type {\n  IncomingMessage,\n  OutgoingHttpHeaders,\n  RequestOptions,\n  ServerResponse,\n} from \"node:http\";\nimport type { RequestOptions as HttpsRequestOptions } from \"node:https\";\nimport type { Socket } from \"node:net\";\nimport type { ProxyServer } from \"../src/server.ts\";\nimport type { ProxyServerOptions, ProxyTargetDetailed } from \"../src/types.ts\";\n\n// --- setupOutgoing ---\n\nexport type OutgoingOptions = Omit<RequestOptions & HttpsRequestOptions, \"headers\"> & {\n  headers?: OutgoingHttpHeaders & Record<string, unknown>;\n};\n\nexport function createOutgoing(): OutgoingOptions {\n  return {};\n}\n\n// --- IncomingMessage stubs ---\n\nexport function stubIncomingMessage(overrides: Record<string, unknown> = {}): IncomingMessage {\n  return {\n    method: \"GET\",\n    url: \"/\",\n    headers: {},\n    httpVersion: \"1.1\",\n    httpVersionMajor: 1,\n    ...overrides,\n  } as unknown as IncomingMessage;\n}\n\n// --- ServerResponse stub ---\n\nexport function stubServerResponse(overrides: Record<string, unknown> = {}): ServerResponse {\n  return overrides as unknown as ServerResponse;\n}\n\n// --- Socket stub ---\n\nexport function stubSocket(overrides: Record<string, unknown> = {}): Socket {\n  return overrides as unknown as Socket;\n}\n\n// --- Middleware options ---\n\nexport type MiddlewareOptions = ProxyServerOptions & {\n  target: URL | ProxyTargetDetailed;\n  forward: URL;\n};\n\nexport function stubMiddlewareOptions(overrides: Record<string, unknown> = {}): MiddlewareOptions {\n  return overrides as unknown as MiddlewareOptions;\n}\n\n// --- ProxyServer stub ---\n\nexport function stubProxyServer(overrides: Record<string, unknown> = {}): ProxyServer<any, any> {\n  return overrides as unknown as ProxyServer;\n}\n"
  },
  {
    "path": "test/_utils.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport * as common from \"../src/_utils.ts\";\nimport { createOutgoing, stubIncomingMessage, stubSocket } from \"./_stubs.ts\";\n\n// Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-http-proxy-common-test.js\n\n// Difference from http-proxy is that we always ensure leading slash on path\n\ndescribe(\"lib/http-proxy/common.js\", () => {\n  describe(\"#setupOutgoing\", () => {\n    it(\"should setup the correct headers\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          agent: \"?\",\n          target: {\n            host: \"hey\",\n            hostname: \"how\",\n            socketPath: \"are\",\n            port: \"you\",\n          },\n          // @ts-expect-error\n          headers: { fizz: \"bang\", overwritten: true },\n          localAddress: \"local.address\",\n          auth: \"username:pass\",\n        },\n        stubIncomingMessage({\n          method: \"i\",\n          url: \"am\",\n          headers: { pro: \"xy\", overwritten: \"false\" },\n        }),\n      );\n\n      expect(outgoing.host).to.eql(\"hey\");\n      expect(outgoing.hostname).to.eql(\"how\");\n      expect(outgoing.socketPath).to.eql(\"are\");\n      expect(outgoing.port).to.eql(\"you\");\n      expect(outgoing.agent).to.eql(\"?\");\n\n      expect(outgoing.method).to.eql(\"i\");\n      expect(outgoing.path).to.eql(\"/am\"); // leading slash is new in httpxy\n\n      expect(outgoing.headers!.pro).to.eql(\"xy\");\n      expect(outgoing.headers!.fizz).to.eql(\"bang\");\n      expect(outgoing.headers!.overwritten).to.eql(true);\n      expect(outgoing.localAddress).to.eql(\"local.address\");\n      expect(outgoing.auth).to.eql(\"username:pass\");\n    });\n\n    it(\"should not overwrite existing ssl options with undefined target values\", () => {\n      const outgoing = createOutgoing();\n      // Simulate options.ssl having cert/key/ca (as passed from web-incoming)\n      Object.assign(outgoing, {\n        cert: \"my-cert\",\n        key: \"my-key\",\n        ca: \"my-ca\",\n      });\n      common.setupOutgoing(\n        outgoing,\n        {\n          agent: undefined,\n          target: {\n            host: \"localhost\",\n            hostname: \"localhost\",\n            port: \"8080\",\n            // No SSL properties on target — they should NOT overwrite outgoing\n          },\n        },\n        stubIncomingMessage({\n          method: \"GET\",\n          url: \"/\",\n          headers: {},\n        }),\n      );\n      // SSL options from outgoing (options.ssl) must be preserved\n      expect(outgoing.cert).to.eql(\"my-cert\");\n      expect(outgoing.key).to.eql(\"my-key\");\n      expect(outgoing.ca).to.eql(\"my-ca\");\n    });\n\n    it(\"should not override agentless upgrade header\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          agent: undefined,\n          target: {\n            host: \"hey\",\n            hostname: \"how\",\n            socketPath: \"are\",\n            port: \"you\",\n          },\n          headers: { connection: \"upgrade\" },\n        },\n        stubIncomingMessage({\n          method: \"i\",\n          url: \"am\",\n          headers: { pro: \"xy\", overwritten: \"false\" },\n        }),\n      );\n      expect(outgoing.headers!.connection).to.eql(\"upgrade\");\n    });\n\n    it(\"should not override agentless connection: contains upgrade\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          agent: undefined,\n          target: {\n            host: \"hey\",\n            hostname: \"how\",\n            socketPath: \"are\",\n            port: \"you\",\n          },\n          headers: { connection: \"keep-alive, upgrade\" }, // this is what Firefox sets\n        },\n        stubIncomingMessage({\n          method: \"i\",\n          url: \"am\",\n          headers: { pro: \"xy\", overwritten: \"false\" },\n        }),\n      );\n      expect(outgoing.headers!.connection).to.eql(\"keep-alive, upgrade\");\n    });\n\n    it(\"should override agentless connection: contains improper upgrade\", () => {\n      // sanity check on upgrade regex\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          agent: false,\n          target: {\n            host: \"hey\",\n            hostname: \"how\",\n            socketPath: \"are\",\n            port: \"you\",\n          },\n          headers: { connection: \"keep-alive, not upgrade\" },\n        },\n        stubIncomingMessage({\n          method: \"i\",\n          url: \"am\",\n          headers: { pro: \"xy\", overwritten: \"false\" },\n        }),\n      );\n      expect(outgoing.headers!.connection).to.eql(\"close\");\n    });\n\n    it(\"should override agentless non-upgrade header to close\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          agent: false,\n          target: {\n            host: \"hey\",\n            hostname: \"how\",\n            socketPath: \"are\",\n            port: \"you\",\n          },\n          headers: { connection: \"xyz\" },\n        },\n        stubIncomingMessage({\n          method: \"i\",\n          url: \"am\",\n          headers: { pro: \"xy\", overwritten: \"false\" },\n        }),\n      );\n      expect(outgoing.headers!.connection).to.eql(\"close\");\n    });\n\n    it(\"should use default keep-alive agent if none is given\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        { target: \"http://localhost\" },\n        stubIncomingMessage({\n          url: \"/\",\n        }),\n      );\n      expect(outgoing.agent).to.eql(common.defaultAgents.http);\n    });\n\n    it(\"should set the agent to false if explicitly set\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        { target: \"http://localhost\", agent: false },\n        stubIncomingMessage({\n          url: \"/\",\n        }),\n      );\n      expect(outgoing.agent).to.eql(false);\n    });\n\n    it(\"should not use keep-alive agent for websocket upgrade requests\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        { target: \"http://localhost\" },\n        stubIncomingMessage({\n          url: \"/\",\n          headers: { connection: \"Upgrade\", upgrade: \"websocket\" },\n        }),\n      );\n      // WebSocket upgrades take ownership of the socket after 101,\n      // so a keep-alive agent must not be used.\n      expect(outgoing.agent).to.eql(false);\n    });\n\n    it(\"set the port according to the protocol\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          agent: \"?\",\n          target: {\n            host: \"how\",\n            hostname: \"are\",\n            socketPath: \"you\",\n            protocol: \"https:\",\n          },\n        },\n        stubIncomingMessage({\n          method: \"i\",\n          url: \"am\",\n          headers: { pro: \"xy\" },\n        }),\n      );\n\n      expect(outgoing.host).to.eql(\"how\");\n      expect(outgoing.hostname).to.eql(\"are\");\n      expect(outgoing.socketPath).to.eql(\"you\");\n      expect(outgoing.agent).to.eql(\"?\");\n\n      expect(outgoing.method).to.eql(\"i\");\n      expect(outgoing.path).to.eql(\"/am\");\n      expect(outgoing.headers!.pro).to.eql(\"xy\");\n\n      expect(outgoing.port).to.eql(443);\n    });\n\n    it(\"should keep the original target path in the outgoing path\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        { target: URL.parse(\"http://localhost/some-path\")! },\n        stubIncomingMessage({\n          url: \"am\",\n        }),\n      );\n\n      expect(outgoing.path).to.eql(\"/some-path/am\");\n    });\n\n    it(\"should keep the original forward path in the outgoing path\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          target: \"http://localhost\",\n          forward: URL.parse(\"http://localhost/some-path\")!,\n        },\n        stubIncomingMessage({\n          url: \"am\",\n        }),\n        \"forward\",\n      );\n\n      expect(outgoing.path).to.eql(\"/some-path/am\");\n    });\n\n    it(\"should properly detect https/wss protocol without the colon\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          target: {\n            protocol: \"https\",\n            host: \"whatever.com\",\n          },\n        },\n        stubIncomingMessage({ url: \"/\" }),\n      );\n\n      expect(outgoing.port).to.eql(443);\n    });\n\n    it(\"should not prepend the target path to the outgoing path with prependPath = false\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          target: URL.parse(\"http://localhost/hellothere\")!,\n          prependPath: false,\n        },\n        stubIncomingMessage({ url: \"hi\" }),\n      );\n\n      expect(outgoing.path).to.eql(\"/hi\");\n    });\n\n    it(\"should properly join paths\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          target: URL.parse(\"http://localhost/forward\")!,\n        },\n        stubIncomingMessage({ url: \"/static/path\" }),\n      );\n\n      expect(outgoing.path).to.eql(\"/forward/static/path\");\n    });\n\n    it(\"should preserve multiple consecutive slashes in path (#80)\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        { target: \"http://localhost:3004\" },\n        stubIncomingMessage({ url: \"//test\" }),\n      );\n      expect(outgoing.path).to.eql(\"//test\");\n    });\n\n    it(\"should preserve multiple consecutive slashes with query string (#80)\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        { target: \"http://localhost:3004\" },\n        stubIncomingMessage({\n          url: \"//test?foo=bar\",\n        }),\n      );\n      expect(outgoing.path).to.eql(\"//test?foo=bar\");\n    });\n\n    it(\"should preserve target query string when merging paths\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          target: URL.parse(\"http://localhost/api?key=val\")!,\n        },\n        stubIncomingMessage({ url: \"/endpoint\" }),\n      );\n\n      expect(outgoing.path).to.eql(\"/api/endpoint?key=val\");\n    });\n\n    it(\"should merge target and request query strings with &\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          target: URL.parse(\"http://localhost/api?key=val\")!,\n        },\n        stubIncomingMessage({ url: \"/endpoint?foo=bar\" }),\n      );\n\n      expect(outgoing.path).to.eql(\"/api/endpoint?key=val&foo=bar\");\n    });\n\n    it(\"should preserve target query string with no request path\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          target: URL.parse(\"http://localhost?project=example&path=\")!,\n        },\n        stubIncomingMessage({ url: \"/\" }),\n      );\n\n      expect(outgoing.path).to.eql(\"/?project=example&path=\");\n    });\n\n    it(\"should not modify the query string\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          target: URL.parse(\"http://localhost/forward\")!,\n        },\n        stubIncomingMessage({\n          url: \"/?foo=bar//&target=http://foobar.com/?a=1%26b=2&other=2\",\n        }),\n      );\n\n      expect(outgoing.path).to.eql(\n        \"/forward/?foo=bar//&target=http://foobar.com/?a=1%26b=2&other=2\",\n      );\n    });\n\n    //\n    // This is the proper failing test case for the common.join problem\n    //\n    it(\"should correctly format the toProxy URL\", () => {\n      const outgoing = createOutgoing();\n      const google = \"https://google.com\";\n      common.setupOutgoing(\n        outgoing,\n        {\n          target: URL.parse(\"http://sometarget.com:80\")!,\n          toProxy: true,\n        },\n        stubIncomingMessage({ url: google }),\n      );\n\n      expect(outgoing.path).to.eql(\"/\" + google);\n    });\n\n    it(\"should not replace :\\\\ to :\\\\\\\\ when no https word before\", () => {\n      const outgoing = createOutgoing();\n      const google = \"https://google.com:/join/join.js\";\n      common.setupOutgoing(\n        outgoing,\n        {\n          target: URL.parse(\"http://sometarget.com:80\")!,\n          toProxy: true,\n        },\n        stubIncomingMessage({ url: google }),\n      );\n\n      expect(outgoing.path).to.eql(\"/\" + google);\n    });\n\n    it(\"should not replace :\\\\ to \\\\\\\\ when no http word before\", () => {\n      const outgoing = createOutgoing();\n      const google = \"http://google.com:/join/join.js\";\n      common.setupOutgoing(\n        outgoing,\n        {\n          target: URL.parse(\"http://sometarget.com:80\")!,\n          toProxy: true,\n        },\n        stubIncomingMessage({ url: google }),\n      );\n\n      expect(outgoing.path).to.eql(\"/\" + google);\n    });\n\n    describe(\"when using ignorePath\", () => {\n      it(\"should ignore the path of the `req.url` passed in but use the target path\", () => {\n        const outgoing = createOutgoing();\n        const myEndpoint = \"https://whatever.com/some/crazy/path/whoooo\";\n        common.setupOutgoing(\n          outgoing,\n          {\n            target: URL.parse(myEndpoint)!,\n            ignorePath: true,\n          },\n          stubIncomingMessage({ url: \"/more/crazy/pathness\" }),\n        );\n\n        expect(outgoing.path).to.eql(\"/some/crazy/path/whoooo\");\n      });\n\n      it(\"and prependPath: false, it should ignore path of target and incoming request\", () => {\n        const outgoing = createOutgoing();\n        const myEndpoint = \"https://whatever.com/some/crazy/path/whoooo\";\n        common.setupOutgoing(\n          outgoing,\n          {\n            target: URL.parse(myEndpoint)!,\n            ignorePath: true,\n            prependPath: false,\n          },\n          stubIncomingMessage({ url: \"/more/crazy/pathness\" }),\n        );\n\n        expect(outgoing.path).to.eql(\"/\");\n      });\n    });\n\n    describe(\"when using changeOrigin\", () => {\n      it(\"should correctly set the port to the host when it is a non-standard port using URL.parse\", () => {\n        const outgoing = createOutgoing();\n        const myEndpoint = \"https://myCouch.com:6984\";\n        common.setupOutgoing(\n          outgoing,\n          {\n            target: URL.parse(myEndpoint)!,\n            changeOrigin: true,\n          },\n          stubIncomingMessage({ url: \"/\" }),\n        );\n\n        expect(outgoing.headers!.host).to.eql(\"mycouch.com:6984\");\n      });\n\n      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)\", () => {\n        const outgoing = createOutgoing();\n        common.setupOutgoing(\n          outgoing,\n          {\n            target: {\n              protocol: \"https:\",\n              host: \"mycouch.com\",\n              port: 6984,\n            },\n            changeOrigin: true,\n          },\n          stubIncomingMessage({ url: \"/\" }),\n        );\n        expect(outgoing.headers!.host).to.eql(\"mycouch.com:6984\");\n      });\n    });\n\n    it(\"should pass through https client parameters\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          agent: \"?\",\n          target: {\n            host: \"how\",\n            hostname: \"are\",\n            socketPath: \"you\",\n            protocol: \"https:\",\n            pfx: \"my-pfx\",\n            key: \"my-key\",\n            passphrase: \"my-passphrase\",\n            cert: \"my-cert\",\n            ca: \"my-ca\",\n            ciphers: \"my-ciphers\",\n            secureProtocol: \"my-secure-protocol\",\n          },\n        },\n        stubIncomingMessage({\n          method: \"i\",\n          url: \"am\",\n        }),\n      );\n\n      expect(outgoing.pfx).eql(\"my-pfx\");\n      expect(outgoing.key).eql(\"my-key\");\n      expect(outgoing.passphrase).eql(\"my-passphrase\");\n      expect(outgoing.cert).eql(\"my-cert\");\n      expect(outgoing.ca).eql(\"my-ca\");\n      expect(outgoing.ciphers).eql(\"my-ciphers\");\n      expect(outgoing.secureProtocol).eql(\"my-secure-protocol\");\n    });\n\n    it(\"should set ca from top-level options\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          target: { host: \"localhost\", protocol: \"https:\" },\n          ca: \"my-top-level-ca\",\n        } as any,\n        stubIncomingMessage({ url: \"/\" }),\n      );\n      expect(outgoing.ca).eql(\"my-top-level-ca\");\n    });\n\n    it(\"should handle overriding the `method` of the http request\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        {\n          target: \"https://whooooo.com\",\n          method: \"POST\",\n        },\n        stubIncomingMessage({ method: \"GET\", url: \"\" }),\n      );\n\n      expect(outgoing.method).eql(\"POST\");\n    });\n\n    it(\"should handle empty pathname target\", () => {\n      const outgoing = createOutgoing();\n      common.setupOutgoing(\n        outgoing,\n        { target: URL.parse(\"http://localhost\")! },\n        stubIncomingMessage({\n          url: \"\",\n        }),\n      );\n\n      expect(outgoing.path).toBe(\"/\");\n    });\n  });\n\n  describe(\"#joinURL\", () => {\n    it(\"should insert slash when base has no trailing slash and path has no leading slash\", () => {\n      expect(common.joinURL(\"foo\", \"bar\")).to.eql(\"foo/bar\");\n    });\n\n    it(\"should return path when base is undefined\", () => {\n      expect(common.joinURL(undefined, \"/path\")).to.eql(\"/path\");\n    });\n\n    it(\"should return base when path is undefined\", () => {\n      expect(common.joinURL(\"/base\", undefined)).to.eql(\"/base\");\n    });\n\n    it(\"should return / when both are undefined\", () => {\n      expect(common.joinURL(undefined, undefined)).to.eql(\"/\");\n    });\n\n    it(\"should strip duplicate slash when both have slash\", () => {\n      expect(common.joinURL(\"/base/\", \"/path\")).to.eql(\"/base/path\");\n    });\n\n    it(\"should concat when base has trailing slash and path has no leading slash\", () => {\n      expect(common.joinURL(\"/base/\", \"path\")).to.eql(\"/base/path\");\n    });\n  });\n\n  describe(\"#rewriteCookieProperty\", () => {\n    it(\"should return original cookie when domain is not in config and no wildcard\", () => {\n      const cookie = \"hello; domain=other.com; path=/\";\n      const result = common.rewriteCookieProperty(cookie, { \"specific.com\": \"new.com\" }, \"domain\");\n      expect(result).to.eql(\"hello; domain=other.com; path=/\");\n    });\n  });\n\n  describe(\"#requiresPort\", () => {\n    it(\"should return false for ftp on port 21\", () => {\n      expect(common.requiresPort(21, \"ftp\")).to.eql(false);\n    });\n\n    it(\"should return true for ftp on non-standard port\", () => {\n      expect(common.requiresPort(8021, \"ftp\")).to.eql(true);\n    });\n\n    it(\"should return false for gopher on port 70\", () => {\n      expect(common.requiresPort(70, \"gopher\")).to.eql(false);\n    });\n\n    it(\"should return true for gopher on non-standard port\", () => {\n      expect(common.requiresPort(8070, \"gopher\")).to.eql(true);\n    });\n\n    it(\"should return false for file protocol\", () => {\n      expect(common.requiresPort(0, \"file\")).to.eql(false);\n      expect(common.requiresPort(8080, \"file\")).to.eql(false);\n    });\n\n    it(\"should return false for unknown protocol on port 0\", () => {\n      expect(common.requiresPort(0, \"unknown\")).to.eql(false);\n    });\n\n    it(\"should return true for unknown protocol on non-zero port\", () => {\n      expect(common.requiresPort(8080, \"unknown\")).to.eql(true);\n    });\n\n    it(\"should return false when port is falsy\", () => {\n      expect(common.requiresPort(0, \"http\")).to.eql(false);\n    });\n\n    it(\"should handle protocol with colon\", () => {\n      expect(common.requiresPort(80, \"http:\")).to.eql(false);\n      expect(common.requiresPort(8080, \"http:\")).to.eql(true);\n    });\n  });\n\n  describe(\"#parseAddr\", () => {\n    it(\"should default to port 80 for http\", () => {\n      expect(common.parseAddr(\"http://localhost\")).to.eql({ host: \"localhost\", port: 80 });\n    });\n\n    it(\"should default to port 443 for https\", () => {\n      expect(common.parseAddr(\"https://localhost\")).to.eql({ host: \"localhost\", port: 443 });\n    });\n\n    it(\"should default to port 443 for wss\", () => {\n      expect(common.parseAddr(\"wss://localhost\")).to.eql({ host: \"localhost\", port: 443 });\n    });\n\n    it(\"should default to port 80 for ws\", () => {\n      expect(common.parseAddr(\"ws://localhost\")).to.eql({ host: \"localhost\", port: 80 });\n    });\n\n    it(\"should use explicit port over protocol default\", () => {\n      expect(common.parseAddr(\"https://localhost:8443\")).to.eql({ host: \"localhost\", port: 8443 });\n    });\n\n    it(\"should parse unix socket path\", () => {\n      expect(common.parseAddr(\"unix:/tmp/sock\")).to.eql({ socketPath: \"/tmp/sock\" });\n    });\n\n    it(\"should pass through a valid ProxyAddr with port\", () => {\n      const addr = { host: \"127.0.0.1\", port: 3000 };\n      expect(common.parseAddr(addr)).to.eql(addr);\n    });\n\n    it(\"should pass through a valid ProxyAddr with socketPath\", () => {\n      const addr = { socketPath: \"/tmp/proxy.sock\" };\n      expect(common.parseAddr(addr)).to.eql(addr);\n    });\n\n    it(\"should throw for ProxyAddr missing port and socketPath\", () => {\n      expect(() => common.parseAddr({} as any)).toThrowError(\n        /ProxyAddr must have either `port` or `socketPath`/,\n      );\n    });\n  });\n\n  describe(\"#setupSocket\", () => {\n    it(\"should setup a socket\", () => {\n      const socketConfig = {\n          timeout: undefined as number | undefined,\n          nodelay: false,\n          keepalive: false,\n        },\n        sock = stubSocket({\n          setTimeout: function (num: number) {\n            socketConfig.timeout = num;\n          },\n          setNoDelay: function (bol: boolean) {\n            socketConfig.nodelay = bol;\n          },\n          setKeepAlive: function (bol: boolean) {\n            socketConfig.keepalive = bol;\n          },\n        });\n      const returnValue = common.setupSocket(sock);\n\n      expect(socketConfig.timeout).to.eql(0);\n      expect(socketConfig.nodelay).to.eql(true);\n      expect(socketConfig.keepalive).to.eql(true);\n    });\n  });\n});\n"
  },
  {
    "path": "test/_utils.ts",
    "content": "import http from \"node:http\";\nimport https from \"node:https\";\nimport net from \"node:net\";\nimport type { AddressInfo } from \"node:net\";\nimport * as httpProxy from \"../src/index.ts\";\n\nexport function listenOn(server: http.Server | https.Server | net.Server): Promise<number> {\n  return new Promise((resolve, reject) => {\n    server.once(\"error\", reject);\n    server.listen(0, \"127.0.0.1\", () => {\n      resolve((server.address() as AddressInfo).port);\n    });\n  });\n}\n\nexport function proxyListen(\n  proxy: ReturnType<typeof httpProxy.createProxyServer>,\n): Promise<number> {\n  return new Promise((resolve, reject) => {\n    proxy.listen(0, \"127.0.0.1\");\n    const server = (proxy as any)._server as net.Server;\n    server.once(\"error\", reject);\n    server.once(\"listening\", () => {\n      resolve((server.address() as AddressInfo).port);\n    });\n  });\n}\n"
  },
  {
    "path": "test/fetch.test.ts",
    "content": "import { createServer, type Server } from \"node:http\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { afterAll, beforeAll, describe, expect, it } from \"vitest\";\n\nimport type { AddressInfo } from \"node:net\";\n\nimport { proxyFetch } from \"../src/fetch.ts\";\n\n// --- TCP server ---\n\nlet tcpServer: Server;\nlet tcpPort: number;\n\nbeforeAll(async () => {\n  tcpServer = createServer((req, res) => {\n    if (req.url === \"/json\") {\n      res.writeHead(200, { \"content-type\": \"application/json\" });\n      res.end(JSON.stringify({ ok: true }));\n      return;\n    }\n    if (req.url === \"/slow\") {\n      const timer = setTimeout(() => {\n        res.writeHead(200, { \"content-type\": \"text/plain\" });\n        res.end(\"slow-ok\");\n      }, 5000);\n      req.on(\"close\", () => clearTimeout(timer));\n      return;\n    }\n    if (req.url === \"/echo\" && req.method === \"POST\") {\n      const chunks: Buffer[] = [];\n      req.on(\"data\", (c) => chunks.push(c));\n      req.on(\"end\", () => {\n        res.writeHead(200, { \"content-type\": \"text/plain\" });\n        res.end(Buffer.concat(chunks));\n      });\n      return;\n    }\n    if (req.url?.startsWith(\"/headers\")) {\n      res.writeHead(200, { \"content-type\": \"application/json\" });\n      res.end(JSON.stringify({ headers: req.headers, url: req.url }));\n      return;\n    }\n    if (req.url === \"/redirect\") {\n      res.writeHead(302, { location: \"/json\" });\n      res.end();\n      return;\n    }\n    if (req.url === \"/redirect-307\") {\n      res.writeHead(307, { location: \"/echo\" });\n      res.end();\n      return;\n    }\n    if (req.url === \"/redirect-chain\") {\n      res.writeHead(301, { location: \"/redirect\" });\n      res.end();\n      return;\n    }\n    if (req.url === \"/multi-cookie\") {\n      res.writeHead(200, [\n        [\"set-cookie\", \"a=1; Path=/\"],\n        [\"set-cookie\", \"b=2; Path=/\"],\n        [\"set-cookie\", \"c=3; Path=/\"],\n        [\"content-type\", \"text/plain\"],\n      ]);\n      res.end(\"ok\");\n      return;\n    }\n    if (req.url === \"/no-content\") {\n      res.writeHead(204);\n      res.end();\n      return;\n    }\n    res.writeHead(404);\n    res.end(\"Not found\");\n  });\n\n  await new Promise<void>((resolve) => {\n    tcpServer.listen(0, \"127.0.0.1\", resolve);\n  });\n  tcpPort = (tcpServer.address() as AddressInfo).port;\n});\n\nafterAll(() => {\n  tcpServer?.close();\n});\n\n// --- Unix socket server ---\n\nlet socketServer: Server;\nconst socketPath = join(tmpdir(), `httpxy-test-${process.pid}-${Date.now()}.sock`);\n\nbeforeAll(async () => {\n  socketServer = createServer((req, res) => {\n    res.writeHead(200, { \"content-type\": \"text/plain\" });\n    res.end(\"unix-ok\");\n  });\n  await new Promise<void>((resolve) => {\n    socketServer.listen(socketPath, resolve);\n  });\n});\n\nafterAll(() => {\n  socketServer?.close();\n});\n\n// --- Tests ---\n\ndescribe(\"proxyFetch\", () => {\n  describe(\"TCP (host + port)\", () => {\n    it(\"GET request returns JSON\", async () => {\n      const res = await proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, `http://localhost/json`);\n      expect(res.status).toBe(200);\n      expect(res.headers.get(\"content-type\")).toBe(\"application/json\");\n      expect(await res.json()).toEqual({ ok: true });\n    });\n\n    it(\"POST with body\", async () => {\n      const res = await proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, `http://localhost/echo`, {\n        method: \"POST\",\n        body: \"hello\",\n      });\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"hello\");\n    });\n\n    it(\"forwards custom headers\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/headers`,\n        { headers: { \"x-custom\": \"test-value\" } },\n      );\n      const body = (await res.json()) as { headers: Record<string, string> };\n      expect(body.headers[\"x-custom\"]).toBe(\"test-value\");\n    });\n\n    it(\"handles redirect manually (no follow)\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/redirect`,\n      );\n      expect(res.status).toBe(302);\n      expect(res.headers.get(\"location\")).toBe(\"/json\");\n    });\n\n    it(\"handles 204 no content\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/no-content`,\n      );\n      expect(res.status).toBe(204);\n      expect(res.body).toBeNull();\n    });\n\n    it(\"preserves multiple set-cookie headers\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/multi-cookie`,\n      );\n      expect(res.status).toBe(200);\n      const cookies = res.headers.getSetCookie();\n      expect(cookies).toEqual([\"a=1; Path=/\", \"b=2; Path=/\", \"c=3; Path=/\"]);\n    });\n\n    it(\"strips hop-by-hop headers\", async () => {\n      const res = await proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, `http://localhost/json`);\n      expect(res.headers.has(\"transfer-encoding\")).toBe(false);\n      expect(res.headers.has(\"keep-alive\")).toBe(false);\n      expect(res.headers.has(\"connection\")).toBe(false);\n    });\n\n    it(\"handles Request object input\", async () => {\n      const req = new Request(\"http://localhost/json\", {\n        headers: { accept: \"application/json\" },\n      });\n      const res = await proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, req);\n      expect(res.status).toBe(200);\n      expect(await res.json()).toEqual({ ok: true });\n    });\n\n    it(\"preserves query string\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/headers?foo=bar`,\n      );\n      expect(res.status).toBe(200);\n      const body = (await res.json()) as { url: string };\n      expect(body.url).toBe(\"/headers?foo=bar\");\n    });\n  });\n\n  describe(\"Unix socket\", () => {\n    it(\"GET via unix socket\", async () => {\n      const res = await proxyFetch({ socketPath }, `http://localhost/anything`);\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"unix-ok\");\n    });\n  });\n\n  describe(\"POST with streaming body\", () => {\n    it(\"pipes ReadableStream body\", async () => {\n      const stream = new ReadableStream({\n        start(controller) {\n          controller.enqueue(new TextEncoder().encode(\"streamed\"));\n          controller.close();\n        },\n      });\n      const res = await proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, `http://localhost/echo`, {\n        method: \"POST\",\n        body: stream,\n      });\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"streamed\");\n    });\n  });\n\n  describe(\"Request as inputInit\", () => {\n    it(\"uses Request object as inputInit\", async () => {\n      const initReq = new Request(\"http://localhost/echo\", {\n        method: \"POST\",\n        body: \"from-request-init\",\n      });\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/echo`,\n        initReq,\n      );\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"from-request-init\");\n    });\n\n    it(\"inputInit Request overrides input Request properties\", async () => {\n      const input = new Request(\"http://localhost/echo\", { method: \"GET\" });\n      const initReq = new Request(\"http://localhost/echo\", {\n        method: \"POST\",\n        headers: { \"x-from\": \"init-request\" },\n        body: \"override-body\",\n      });\n      const res = await proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, input, initReq);\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"override-body\");\n    });\n\n    it(\"inputInit Request with streaming body\", async () => {\n      const stream = new ReadableStream({\n        start(controller) {\n          controller.enqueue(new TextEncoder().encode(\"init-stream\"));\n          controller.close();\n        },\n      });\n      const initReq = new Request(\"http://localhost/echo\", {\n        method: \"POST\",\n        body: stream,\n        // @ts-expect-error duplex\n        duplex: \"half\",\n      });\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/echo`,\n        initReq,\n      );\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"init-stream\");\n    });\n  });\n\n  describe(\"Request input with body\", () => {\n    it(\"POST body from Request input\", async () => {\n      const req = new Request(\"http://localhost/echo\", {\n        method: \"POST\",\n        body: \"request-body\",\n      });\n      const res = await proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, req);\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"request-body\");\n    });\n\n    it(\"POST streaming body from Request input\", async () => {\n      const stream = new ReadableStream({\n        start(controller) {\n          controller.enqueue(new TextEncoder().encode(\"req-stream\"));\n          controller.close();\n        },\n      });\n      const req = new Request(\"http://localhost/echo\", {\n        method: \"POST\",\n        body: stream,\n        // @ts-expect-error duplex\n        duplex: \"half\",\n      });\n      const res = await proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, req);\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"req-stream\");\n    });\n  });\n\n  describe(\"string addr\", () => {\n    it(\"accepts http URL string as addr\", async () => {\n      const res = await proxyFetch(`http://127.0.0.1:${tcpPort}`, `http://localhost/json`);\n      expect(res.status).toBe(200);\n      expect(await res.json()).toEqual({ ok: true });\n    });\n\n    it(\"accepts unix: string as addr\", async () => {\n      const res = await proxyFetch(`unix:${socketPath}`, `http://localhost/anything`);\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"unix-ok\");\n    });\n  });\n\n  describe(\"error handling\", () => {\n    it(\"rejects on connection error\", async () => {\n      await expect(\n        proxyFetch({ host: \"127.0.0.1\", port: 1 }, `http://localhost/`),\n      ).rejects.toThrow();\n    });\n\n    it(\"rejects on body stream error\", async () => {\n      const stream = new ReadableStream({\n        start(controller) {\n          controller.error(new Error(\"stream-fail\"));\n        },\n      });\n      await expect(\n        proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, `http://localhost/echo`, {\n          method: \"POST\",\n          body: stream,\n        }),\n      ).rejects.toThrow(\"stream-fail\");\n    });\n  });\n\n  describe(\"signal / abort\", () => {\n    it(\"aborts request with already-aborted signal\", async () => {\n      const controller = new AbortController();\n      controller.abort();\n      await expect(\n        proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, `http://localhost/json`, {\n          signal: controller.signal,\n        }),\n      ).rejects.toThrow();\n    });\n\n    it(\"aborts in-flight request\", async () => {\n      const controller = new AbortController();\n      setTimeout(() => controller.abort(), 50);\n      await expect(\n        proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, `http://localhost/slow`, {\n          signal: controller.signal,\n        }),\n      ).rejects.toThrow();\n    });\n\n    it(\"succeeds when signal is not aborted\", async () => {\n      const controller = new AbortController();\n      const res = await proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, `http://localhost/json`, {\n        signal: controller.signal,\n      });\n      expect(res.status).toBe(200);\n    });\n  });\n\n  describe(\"timeout\", () => {\n    it(\"rejects when upstream does not respond in time\", async () => {\n      await expect(\n        proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, `http://localhost/slow`, undefined, {\n          timeout: 50,\n        }),\n      ).rejects.toThrow(\"Proxy request timed out\");\n    });\n\n    it(\"succeeds when response arrives before timeout\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/json`,\n        undefined,\n        { timeout: 5000 },\n      );\n      expect(res.status).toBe(200);\n    });\n  });\n\n  describe(\"xfwd\", () => {\n    it(\"adds x-forwarded-* headers when enabled\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://example.com:3000/headers`,\n        undefined,\n        { xfwd: true },\n      );\n      const body = (await res.json()) as { headers: Record<string, string> };\n      expect(body.headers[\"x-forwarded-for\"]).toBe(\"example.com\");\n      expect(body.headers[\"x-forwarded-port\"]).toBe(\"3000\");\n      expect(body.headers[\"x-forwarded-proto\"]).toBe(\"http\");\n      expect(body.headers[\"x-forwarded-host\"]).toBe(\"example.com:3000\");\n    });\n\n    it(\"does not add x-forwarded-* headers by default\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/headers`,\n      );\n      const body = (await res.json()) as { headers: Record<string, string> };\n      expect(body.headers[\"x-forwarded-for\"]).toBeUndefined();\n      expect(body.headers[\"x-forwarded-port\"]).toBeUndefined();\n      expect(body.headers[\"x-forwarded-proto\"]).toBeUndefined();\n      expect(body.headers[\"x-forwarded-host\"]).toBeUndefined();\n    });\n\n    it(\"does not overwrite existing x-forwarded-* headers\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://example.com/headers`,\n        { headers: { \"x-forwarded-for\": \"10.0.0.1\" } },\n        { xfwd: true },\n      );\n      const body = (await res.json()) as { headers: Record<string, string> };\n      expect(body.headers[\"x-forwarded-for\"]).toBe(\"10.0.0.1\");\n    });\n  });\n\n  describe(\"changeOrigin\", () => {\n    it(\"rewrites Host header to target address\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://original-host.com/headers`,\n        undefined,\n        { changeOrigin: true },\n      );\n      const body = (await res.json()) as { headers: Record<string, string> };\n      expect(body.headers.host).toBe(`127.0.0.1:${tcpPort}`);\n    });\n\n    it(\"keeps original Host header when changeOrigin is false\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://original-host.com/headers`,\n        { headers: { host: \"original-host.com\" } },\n      );\n      const body = (await res.json()) as { headers: Record<string, string> };\n      expect(body.headers.host).toBe(\"original-host.com\");\n    });\n\n    it(\"uses localhost for unix socket targets\", async () => {\n      const unixHeaders = createServer((req, res) => {\n        res.writeHead(200, { \"content-type\": \"application/json\" });\n        res.end(JSON.stringify({ host: req.headers.host }));\n      });\n      const tmpSocket = join(tmpdir(), `httpxy-co-${process.pid}-${Date.now()}.sock`);\n      await new Promise<void>((resolve) => unixHeaders.listen(tmpSocket, resolve));\n      try {\n        const res = await proxyFetch(\n          { socketPath: tmpSocket },\n          `http://original-host.com/`,\n          undefined,\n          { changeOrigin: true },\n        );\n        const body = (await res.json()) as { host: string };\n        expect(body.host).toBe(\"localhost\");\n      } finally {\n        unixHeaders.close();\n      }\n    });\n  });\n\n  describe(\"agent\", () => {\n    it(\"uses provided agent for connection pooling\", async () => {\n      const { Agent } = await import(\"node:http\");\n      const agent = new Agent({ keepAlive: true });\n      try {\n        const res = await proxyFetch(\n          { host: \"127.0.0.1\", port: tcpPort },\n          `http://localhost/json`,\n          undefined,\n          { agent },\n        );\n        expect(res.status).toBe(200);\n        expect(await res.json()).toEqual({ ok: true });\n      } finally {\n        agent.destroy();\n      }\n    });\n  });\n\n  describe(\"body types\", () => {\n    it(\"sends ArrayBuffer body\", async () => {\n      const buf = new TextEncoder().encode(\"arraybuffer-body\");\n      const res = await proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, `http://localhost/echo`, {\n        method: \"POST\",\n        body: buf.buffer,\n      });\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"arraybuffer-body\");\n    });\n\n    it(\"sends Uint8Array body\", async () => {\n      const buf = new TextEncoder().encode(\"uint8-body\");\n      const res = await proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, `http://localhost/echo`, {\n        method: \"POST\",\n        body: buf,\n      });\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"uint8-body\");\n    });\n\n    it(\"sends Blob body\", async () => {\n      const blob = new Blob([\"blob-body\"], { type: \"text/plain\" });\n      const res = await proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, `http://localhost/echo`, {\n        method: \"POST\",\n        body: blob,\n      });\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"blob-body\");\n    });\n  });\n\n  describe(\"multi-value request headers\", () => {\n    it(\"preserves multiple cookie values\", async () => {\n      const headers = new Headers();\n      headers.append(\"x-multi\", \"val1\");\n      headers.append(\"x-multi\", \"val2\");\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/headers`,\n        { headers },\n      );\n      const body = (await res.json()) as { headers: Record<string, string> };\n      // Node.js http server joins multi-value headers with \", \"\n      expect(body.headers[\"x-multi\"]).toBe(\"val1, val2\");\n    });\n  });\n\n  describe(\"followRedirects\", () => {\n    it(\"follows 302 redirect and returns final response\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/redirect`,\n        undefined,\n        { followRedirects: true },\n      );\n      expect(res.status).toBe(200);\n      expect(await res.json()).toEqual({ ok: true });\n    });\n\n    it(\"follows redirect chain\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/redirect-chain`,\n        undefined,\n        { followRedirects: true },\n      );\n      expect(res.status).toBe(200);\n      expect(await res.json()).toEqual({ ok: true });\n    });\n\n    it(\"preserves method and body on 307 redirect\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/redirect-307`,\n        { method: \"POST\", body: \"preserved\" },\n        { followRedirects: true },\n      );\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"preserved\");\n    });\n\n    it(\"respects custom max redirects\", async () => {\n      // redirect-chain → redirect → json (2 hops), limit to 1\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/redirect-chain`,\n        undefined,\n        { followRedirects: 1 },\n      );\n      // After 1 hop we land on /redirect which is a 302 — returned as-is\n      expect(res.status).toBe(302);\n    });\n\n    it(\"returns raw 3xx when followRedirects is false\", async () => {\n      const res = await proxyFetch(\n        { host: \"127.0.0.1\", port: tcpPort },\n        `http://localhost/redirect`,\n        undefined,\n        { followRedirects: false },\n      );\n      expect(res.status).toBe(302);\n      expect(res.headers.get(\"location\")).toBe(\"/json\");\n    });\n  });\n\n  describe(\"path merging\", () => {\n    it(\"prepends addr base path to request path\", async () => {\n      const res = await proxyFetch(\n        `http://127.0.0.1:${tcpPort}/headers`,\n        `http://localhost/?from=merge`,\n      );\n      expect(res.status).toBe(200);\n      const body = (await res.json()) as { url: string };\n      expect(body.url).toBe(\"/headers/?from=merge\");\n    });\n\n    it(\"joins addr base path with request subpath\", async () => {\n      // addr has /headers, request has /sub → merged to /headers/sub\n      // server matches startsWith(\"/headers\") so this works\n      const res = await proxyFetch(`http://127.0.0.1:${tcpPort}/headers`, `http://localhost/sub`);\n      expect(res.status).toBe(200);\n      const body = (await res.json()) as { url: string };\n      expect(body.url).toBe(\"/headers/sub\");\n    });\n\n    it(\"uses request path when addr has no path\", async () => {\n      const res = await proxyFetch(`http://127.0.0.1:${tcpPort}`, `http://localhost/json`);\n      expect(res.status).toBe(200);\n      expect(await res.json()).toEqual({ ok: true });\n    });\n  });\n\n  describe(\"HTTPS upstream\", () => {\n    it(\"detects HTTPS from addr string\", async () => {\n      // We can't easily test real HTTPS without certs, but we can verify\n      // that an https addr doesn't throw and properly rejects on connection\n      // (which proves httpsRequest was selected, not httpRequest)\n      await expect(proxyFetch(`https://127.0.0.1:1`, `http://localhost/json`)).rejects.toThrow();\n    });\n\n    it(\"uses HTTP by default for object addr\", async () => {\n      const res = await proxyFetch({ host: \"127.0.0.1\", port: tcpPort }, `http://localhost/json`);\n      expect(res.status).toBe(200);\n    });\n  });\n});\n"
  },
  {
    "path": "test/fixtures/agent2-cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEIDCCAggCCQChRDh/XiBF+zANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJ1\nczETMBEGA1UECAwKV2FzaGluZ3RvbjEQMA4GA1UEBwwHU2VhdHRsZTEeMBwGA1UE\nAwwVRHVtbXkgSW50ZXJtZWRpYXRlIENBMB4XDTE4MDYyMjIwMzEwNFoXDTMyMDIy\nOTIwMzEwNFowUDELMAkGA1UEBhMCdXMxEzARBgNVBAgMCldhc2hpbmd0b24xEDAO\nBgNVBAcMB1NlYXR0bGUxGjAYBgNVBAMMEWR1bW15LmV4YW1wbGUuY29tMIIBIjAN\nBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvSQq3d8AeZMTvtqZ13jWCckikyXJ\nSACvkGCQUCJqOceESbg6IHdRzQdoccE4P3sbvNsf9BlbdJKM+neCxabqKaU1PPje\n4P0tHT57t6yJrMuUh9NxEz3Bgh1srNHVS7saKvwHmcKm79jc+wxlioPmEQvQagjn\ny7oTkyLt0sn4LGxBjrcv2JoHOC9f1pxX7l47MaiN0/ctRau7Nr3PFn+pkB4Yf6Z0\nVyicVJbaUSz39Qo4HQWl1L2hiBP3CS1oKs2Yk0O1aOCMExWrhZQan+ZgHqL1rhgm\nkPpw2/zwwPt5Vf9CSakvHwg198EXuTTXtkzYduuIJAm8yp969iEIiG2xTwIDAQAB\nMA0GCSqGSIb3DQEBCwUAA4ICAQBnMSIo+kujkeXPh+iErFBmNtu/7EA+i/QnFPbN\nlSLngclYYBJAGQI+DhirJI8ghDi6vmlHB2THewDaOJXEKvC1czE8064wioIcA9HJ\nl3QJ3YYOFRctYdSHBU4TWdJbPgkLWDzYP5smjOfw8nDdr4WO/5jh9qRFcFpTFmQf\nDyU3xgWLsQnNK3qXLdJjWG75pEhHR+7TGo+Ob/RUho/1RX/P89Ux7/oVbzdKqqFu\nSErXAsjEIEFzWOM2uDOt6hrxDF6q+8/zudwQNEo422poEcTT9tDEFxMQ391CzZRi\nnozBm4igRn1f5S3YZzLI6VEUns0s76BNy2CzvFWn40DziTqNBExAMfFFj76wiMsX\n6fTIdcvkaTBa0S9SZB0vN99qahBdcG17rt4RssMHVRH1Wn7NXMwe476L0yXZ6gO7\nZ4uNAPxgaI3BRP75EPfslLutCLZ+BC4Zzu6MY0Salbpfl0Go462EhsKCxvYhE2Dg\nT477pICLfETZfA499Fd1tOaIsoLCrILAia/+Yd76uf94MuXUIqykDng/4H7xCc47\nBZhNFJiPC6XHaXzN7NYSEUNX9VOwY8ncxKwtP6TXga96PdMUy/p98KIM8RZlDoxB\nXy9dcZBFNn/zrqjW7R0CCWCUriDIFSmEP0wDZ91YYa6BVuJMb5uL/USkTLpjZS4/\nHNGvug==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "test/fixtures/agent2-key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpQIBAAKCAQEAvSQq3d8AeZMTvtqZ13jWCckikyXJSACvkGCQUCJqOceESbg6\nIHdRzQdoccE4P3sbvNsf9BlbdJKM+neCxabqKaU1PPje4P0tHT57t6yJrMuUh9Nx\nEz3Bgh1srNHVS7saKvwHmcKm79jc+wxlioPmEQvQagjny7oTkyLt0sn4LGxBjrcv\n2JoHOC9f1pxX7l47MaiN0/ctRau7Nr3PFn+pkB4Yf6Z0VyicVJbaUSz39Qo4HQWl\n1L2hiBP3CS1oKs2Yk0O1aOCMExWrhZQan+ZgHqL1rhgmkPpw2/zwwPt5Vf9CSakv\nHwg198EXuTTXtkzYduuIJAm8yp969iEIiG2xTwIDAQABAoIBAGPIw/C/qJF7HYyv\n6T+7GTiaa2o0IiehbP3/Y8NTFLWc49a8obXlHTvMr7Zr2I/tE+ojtIzkH9K1SjkN\neelqsNj9tsOPDI6oIvftsflpxkxqLtclnt8m0oMhoObf4OaONDT/N8dP4SBiSdsM\nZDmacnMFx5NZVWiup4sVf2CYexx7qks9FhyN2K5PArCQ4S9LHjFhSJVH4DSEpv7E\nYkbp30rhpqV7wSwjgUsm8ZYvI2NOlmffzLSiPdt3vy2K5Q25S/MVEAicg83rfDgK\n6EluHjeygRI1xU6DJ0hU7tnU7zE9KURoHPUycO3BKzZnzUH26AA36I58Pu4fXWw/\nCgmbv2ECgYEA+og9E4ziKCEi3p8gqjIfwTRgWZxDLjEzooB/K0UhEearn/xiX29A\nFiSzEHKfCB4uSrw5OENg2ckDs8uy08Qmxx7xFXL7AtufAl5fIYaWa0sNSqCaIk7p\nebbUmPcaYhKiLzIEd1EYEL38sXVZ62wvSVMRSWvEMq44g1qnoRlDa/8CgYEAwUTt\ntalYNwVmR9ZdkVEWm9ZxirdzoM6NaM6u4Tf34ygptpapdmIFSUhfq4iOiEnRGNg/\ntuNqhNCIb3LNpJbhRPEzqN7E7qiF/mp7AcJgbuxLZBm12QuLuJdG3nrisKPFXcY1\nlA4A7CFmNgH3E4THFfgwzyDXsBOxVLXleTqn+rECgYEA9up1P6J3dtOJuV2d5P/3\nugRz/X173LfTSxJXw36jZDAy8D/feG19/RT4gnplcKvGNhQiVOhbOOnbw0U8n2fQ\nTCmbs+cZqyxnH/+AxNsPvvk+RVHZ93xMsY/XIldP4l65B8jFDA+Zp06IESI2mEeM\npzi+bd1Phh+dRSCA2865W2MCgYEAlxYsgmQ1WyX0dFpHYU+zzfXRYzDQyrhOYc2Z\nduVK+yCto1iad7pfCY/zgmRJkI+sT7DV9kJIRjXDQuTLkEyHJF8vFGe6KhxCS8aw\nDIsI2g4NTd6vg1J8UryoIUqNpqsQoqNNxUVBQVdG0ReuMGsPO8R/W50AIFz0txVP\no/rP0LECgYEA7e/mOwCnR+ovmS/CAksmos3oIqvkRkXNKpKe513FVmp3TpTU38ex\ncBkFNU3hEO31FyrX1hGIKp3N5mHYSQ1lyODHM6teHW0OLWWTwIe8rIGvR2jfRLe0\nbbkdj40atYVkfeFmpz9uHHG24CUYxJdPc360jbXTVp4i3q8zqgL5aMY=\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "test/http-proxy.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport * as httpProxy from \"../src/index.ts\";\nimport http from \"node:http\";\nimport net from \"node:net\";\nimport * as ws from \"ws\";\nimport * as io from \"socket.io\";\nimport SSE from \"sse\";\nimport ioClient from \"socket.io-client\";\nimport type { AddressInfo } from \"node:net\";\nimport { listenOn, proxyListen } from \"./_utils.ts\";\n\n// Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-http-proxy-test.js\n\ndescribe(\"http-proxy\", () => {\n  describe(\"#createProxyServer\", () => {\n    it.skip(\"should throw without options\", () => {\n      let error;\n      try {\n        httpProxy.createProxyServer();\n      } catch (error_) {\n        error = error_;\n      }\n\n      expect(error).to.toBeInstanceOf(Error);\n    });\n\n    it(\"should return an object otherwise\", () => {\n      const obj = httpProxy.createProxyServer({\n        target: \"http://www.google.com:80\",\n      });\n\n      expect(obj.web).to.toBeInstanceOf(Function);\n      expect(obj.ws).to.instanceOf(Function);\n      expect(obj.listen).to.instanceOf(Function);\n    });\n  });\n\n  describe(\"#createProxyServer with forward options and using web-incoming passes\", () => {\n    it(\"should pipe the request using web-incoming#stream method\", async () => {\n      const source = http.createServer();\n      const sourcePort = await listenOn(source);\n\n      const proxy = httpProxy.createProxyServer({\n        forward: \"http://127.0.0.1:\" + sourcePort,\n      });\n      const proxyPort = await proxyListen(proxy);\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      source.on(\"request\", (req, res) => {\n        expect(req.method).to.eql(\"GET\");\n        expect(Number.parseInt(req.headers.host!.split(\":\")[1]!)).toBe(proxyPort);\n        source.close();\n        proxy.close(resolve);\n      });\n\n      http.request(\"http://127.0.0.1:\" + proxyPort, () => {}).end();\n\n      await promise;\n    });\n  });\n\n  describe(\"#createProxyServer using the web-incoming passes\", () => {\n    it(\"should proxy sse\", async () => {\n      const source = http.createServer();\n      const sourcePort = await listenOn(source);\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"http://127.0.0.1:\" + sourcePort,\n      });\n      const proxyPort = await proxyListen(proxy);\n\n      const sse = new SSE(source, { path: \"/\" });\n      sse.on(\"connection\", (client) => {\n        client.send(\"Hello over SSE\");\n        client.close();\n      });\n\n      const options = {\n        hostname: \"127.0.0.1\",\n        port: proxyPort,\n      };\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      const req = http\n        .request(options, (res) => {\n          let streamData = \"\";\n          res.on(\"data\", (chunk) => {\n            streamData += chunk.toString(\"utf8\");\n          });\n          res.on(\"end\", () => {\n            expect(streamData).to.equal(\":ok\\n\\ndata: Hello over SSE\\n\\n\");\n            source.close();\n            proxy.close(resolve);\n          });\n        })\n        .end();\n\n      await promise;\n    });\n\n    it(\"should close downstream SSE stream when upstream aborts\", async () => {\n      const source = http.createServer((_, res) => {\n        res.writeHead(200, {\n          \"content-type\": \"text/event-stream\",\n          \"cache-control\": \"no-cache\",\n          connection: \"keep-alive\",\n        });\n        res.write(\":ok\\n\\n\");\n\n        setTimeout(() => {\n          res.socket?.destroy();\n        }, 20);\n      });\n      const sourcePort = await listenOn(source);\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"http://127.0.0.1:\" + sourcePort,\n      });\n      const proxyPort = await proxyListen(proxy);\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      let gotFirstChunk = false;\n      let requestError: Error | undefined;\n\n      const finish = () => {\n        source.close();\n        proxy.close(resolve);\n      };\n\n      const timeout = setTimeout(() => {\n        requestError = new Error(\"Timed out waiting for downstream SSE close\");\n        finish();\n      }, 1000);\n\n      http\n        .request(\n          {\n            hostname: \"127.0.0.1\",\n            port: proxyPort,\n            method: \"GET\",\n          },\n          (res) => {\n            res.on(\"data\", (chunk) => {\n              if (chunk.toString(\"utf8\").includes(\":ok\")) {\n                gotFirstChunk = true;\n              }\n            });\n\n            res.once(\"close\", () => {\n              clearTimeout(timeout);\n              finish();\n            });\n          },\n        )\n        .on(\"error\", (error) => {\n          clearTimeout(timeout);\n          requestError = error;\n          finish();\n        })\n        .end();\n\n      await promise;\n      expect(requestError).toBeUndefined();\n      expect(gotFirstChunk).toBe(true);\n    });\n\n    it(\"should destroy upstream proxy request when client aborts\", async () => {\n      const { promise, resolve, reject } = Promise.withResolvers<void>();\n\n      // Track whether the upstream request was properly destroyed\n      let upstreamReqDestroyed = false;\n\n      const source = http.createServer((req, res) => {\n        // SSE-like long-lived response\n        res.writeHead(200, {\n          \"content-type\": \"text/event-stream\",\n          \"cache-control\": \"no-cache\",\n          connection: \"keep-alive\",\n        });\n        res.write(\":ok\\n\\n\");\n\n        req.socket.on(\"close\", () => {\n          upstreamReqDestroyed = true;\n        });\n      });\n      const sourcePort = await listenOn(source);\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"http://127.0.0.1:\" + sourcePort,\n      });\n      const proxyPort = await proxyListen(proxy);\n\n      const timeout = setTimeout(() => {\n        reject(new Error(\"Timed out: upstream request was not destroyed after client abort\"));\n      }, 2000);\n\n      // Make a request and abort it after receiving the first chunk\n      const clientReq = http.request(\n        { hostname: \"127.0.0.1\", port: proxyPort, method: \"GET\" },\n        (res) => {\n          res.once(\"data\", () => {\n            // Client received data, now abort the connection\n            clientReq.destroy();\n          });\n        },\n      );\n      clientReq.end();\n\n      // Poll for upstream destruction\n      const check = setInterval(() => {\n        if (upstreamReqDestroyed) {\n          clearInterval(check);\n          clearTimeout(timeout);\n          source.close();\n          proxy.close(() => resolve());\n        }\n      }, 20);\n\n      await promise;\n      expect(upstreamReqDestroyed).toBe(true);\n    });\n\n    it(\"should make the request on pipe and finish it\", async () => {\n      const source = http.createServer();\n      const sourcePort = await listenOn(source);\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"http://127.0.0.1:\" + sourcePort,\n      });\n      const proxyPort = await proxyListen(proxy);\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      source.on(\"request\", (req, res) => {\n        expect(req.method).to.eql(\"POST\");\n        expect(req.headers[\"x-forwarded-for\"]).to.eql(\"127.0.0.1\");\n        expect(Number.parseInt(req.headers.host!.split(\":\")[1]!)).to.eql(proxyPort);\n        source.close();\n        proxy.close(() => {});\n        resolve();\n      });\n\n      http\n        .request(\n          {\n            hostname: \"127.0.0.1\",\n            port: proxyPort,\n            method: \"POST\",\n            headers: {\n              \"x-forwarded-for\": \"127.0.0.1\",\n            },\n          },\n          () => {},\n        )\n        .end();\n\n      await promise;\n    });\n  });\n\n  describe(\"#createProxyServer using the web-incoming passes\", () => {\n    it(\"should make the request, handle response and finish it\", async () => {\n      const source = http.createServer((req, res) => {\n        expect(req.method).to.eql(\"GET\");\n        expect(Number.parseInt(req.headers.host!.split(\":\")[1]!)).to.eql(proxyPort);\n        res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n        res.end(\"Hello from \" + (source.address()! as any).port);\n      });\n      const sourcePort = await listenOn(source);\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"http://127.0.0.1:\" + sourcePort,\n        preserveHeaderKeyCase: true,\n      });\n      const proxyPort = await proxyListen(proxy);\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      http\n        .request(\n          {\n            hostname: \"127.0.0.1\",\n            port: proxyPort,\n            method: \"GET\",\n          },\n          (res) => {\n            expect(res.statusCode).to.eql(200);\n            expect(res.headers[\"content-type\"]).to.eql(\"text/plain\");\n            if (res.rawHeaders != undefined) {\n              expect(res.rawHeaders.indexOf(\"Content-Type\")).not.to.eql(-1);\n              expect(res.rawHeaders.indexOf(\"text/plain\")).not.to.eql(-1);\n            }\n\n            res.on(\"data\", (data) => {\n              expect(data.toString()).to.eql(\"Hello from \" + sourcePort);\n            });\n\n            res.on(\"end\", () => {\n              source.close();\n              proxy.close(resolve);\n            });\n          },\n        )\n        .end();\n      await promise;\n    });\n  });\n\n  describe(\"#createProxyServer() method with error response\", () => {\n    it(\"should make the request and emit the error event\", async () => {\n      const proxy = httpProxy.createProxyServer({\n        target: \"http://127.0.0.1:1\",\n      });\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      proxy.on(\"error\", (err) => {\n        expect(err).toBeInstanceOf(Error);\n        expect((err as any).code).toBe(\"ECONNREFUSED\");\n        proxy.close(() => {});\n        resolve();\n      });\n\n      const proxyPort = await proxyListen(proxy);\n\n      http\n        .request(\n          {\n            hostname: \"127.0.0.1\",\n            port: proxyPort,\n            method: \"GET\",\n          },\n          () => {},\n        )\n        .end();\n\n      await promise.catch(() => {});\n    });\n  });\n\n  describe(\"#createProxyServer setting the correct timeout value\", () => {\n    it(\"should hang up the socket at the timeout\", async () => {\n      const { promise, resolve } = Promise.withResolvers<void>();\n\n      const source = http.createServer(function (_req, res) {\n        setTimeout(() => {\n          res.end(\"At this point the socket should be closed\");\n        }, 5);\n      });\n      const sourcePort = await listenOn(source);\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"http://127.0.0.1:\" + sourcePort,\n        timeout: 3,\n      });\n      const proxyPort = await proxyListen(proxy);\n\n      proxy.on(\"error\", (err) => {\n        expect(err).toBeInstanceOf(Error);\n        expect((err as any).code).toBe(\"ECONNRESET\");\n      });\n\n      const testReq = http.request(\n        {\n          hostname: \"127.0.0.1\",\n          port: proxyPort,\n          method: \"GET\",\n        },\n        () => {},\n      );\n\n      testReq.on(\"error\", (err) => {\n        expect(err).toBeInstanceOf(Error);\n        expect((err as any).code).toBe(\"ECONNRESET\");\n        proxy.close(() => {});\n        source.close();\n        resolve();\n      });\n\n      testReq.end();\n      await promise;\n    });\n  });\n\n  describe(\"#createProxyServer client disconnect\", () => {\n    it(\"should emit econnreset instead of error when client disconnects\", async () => {\n      const { promise, resolve, reject } = Promise.withResolvers<void>();\n\n      // Target server that accepts connections but never responds\n      const source = net.createServer();\n      const sourcePort = await listenOn(source);\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"http://127.0.0.1:\" + sourcePort,\n      });\n\n      // Intercept proxyReq to simulate the race condition where the client\n      // has disconnected (socket is no longer writable) but req.socket.destroyed\n      // is still false — reproducing the timing issue from upstream PR #1542.\n      proxy.on(\"proxyReq\", (proxyReq, req) => {\n        setTimeout(() => {\n          const socket = req.socket;\n          Object.defineProperty(socket, \"writable\", { value: false, configurable: true });\n          Object.defineProperty(socket, \"destroyed\", { value: false, configurable: true });\n          proxyReq.emit(\n            \"error\",\n            Object.assign(new Error(\"read ECONNRESET\"), { code: \"ECONNRESET\" }),\n          );\n        }, 50);\n      });\n\n      const proxyServer = http.createServer((req, res) => {\n        proxy.web(req, res);\n      });\n      const proxyPort = await listenOn(proxyServer);\n\n      proxy.on(\"econnreset\", (err) => {\n        expect(err).toBeInstanceOf(Error);\n        expect((err as any).code).toBe(\"ECONNRESET\");\n        proxy.close(() => {});\n        proxyServer.close();\n        source.close();\n        resolve();\n      });\n\n      proxy.on(\"error\", (err) => {\n        proxy.close(() => {});\n        proxyServer.close();\n        source.close();\n        reject(new Error(`Unexpected error event: ${(err as any).code || err.message}`));\n      });\n\n      const testReq = http.request(\n        {\n          hostname: \"127.0.0.1\",\n          port: proxyPort,\n          method: \"GET\",\n        },\n        () => {},\n      );\n\n      testReq.on(\"error\", () => {\n        // Expected\n      });\n\n      testReq.end();\n\n      await promise;\n    });\n  });\n\n  describe(\"#createProxyServer with xfwd option\", () => {\n    it(\"should not throw on empty http host header\", async () => {\n      const source = http.createServer();\n      const sourcePort = await listenOn(source);\n\n      const proxy = httpProxy.createProxyServer({\n        forward: \"http://127.0.0.1:\" + sourcePort,\n        xfwd: true,\n      });\n      const proxyPort = await proxyListen(proxy);\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      source.on(\"request\", function (req, _res) {\n        expect(req.method).to.eql(\"GET\");\n        // Host header is forwarded from the original request (not changed to source)\n        expect(req.headers[\"x-forwarded-for\"]).toBeDefined();\n        source.close();\n        proxy.close(resolve);\n      });\n\n      const socket = net.connect({ port: proxyPort }, () => {\n        socket.write(\"GET / HTTP/1.0\\r\\n\\r\\n\");\n      });\n\n      socket.on(\"data\", () => {\n        socket.end();\n      });\n\n      // Ignore socket errors during teardown (server may close before socket drains)\n      socket.on(\"error\", () => {});\n\n      http.request(\"http://127.0.0.1:\" + proxyPort, () => {}).end();\n      await promise;\n    });\n  });\n\n  describe(\"#createProxyServer using the ws-incoming passes\", () => {\n    it(\"should proxy the websockets stream\", async () => {\n      const destiny = new ws.WebSocketServer({ port: 0 });\n      await new Promise<void>((r) => destiny.on(\"listening\", r));\n      const sourcePort = (destiny.address() as AddressInfo).port;\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"ws://127.0.0.1:\" + sourcePort,\n        ws: true,\n      });\n      const proxyPort = await proxyListen(proxy);\n      const proxyServer = proxy;\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n      client.on(\"open\", () => {\n        client.send(\"hello there\");\n      });\n\n      client.on(\"message\", (msg) => {\n        expect(msg.toString(\"utf8\")).toBe(\"Hello over websockets\");\n        client.close();\n        destiny.close();\n        proxyServer.close(resolve);\n      });\n\n      destiny.on(\"connection\", (socket) => {\n        socket.on(\"message\", (msg) => {\n          expect(msg.toString(\"utf8\")).toBe(\"hello there\");\n          socket.send(\"Hello over websockets\");\n        });\n      });\n\n      await promise;\n    });\n\n    it(\"should emit error on proxy error\", async () => {\n      const { promise, resolve } = Promise.withResolvers<void>();\n\n      const proxy = httpProxy.createProxyServer({\n        // Note: we don't ever listen on this port\n        target: \"ws://127.0.0.1:1\",\n        ws: true,\n      });\n      const proxyPort = await proxyListen(proxy);\n      const proxyServer = proxy;\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n      client.on(\"open\", () => {\n        client.send(\"hello there\");\n      });\n\n      let count = 0;\n      function maybe_done() {\n        count += 1;\n        if (count === 2) resolve();\n      }\n\n      client.on(\"error\", (err) => {\n        expect(err).toBeInstanceOf(Error);\n        expect((err as any).code).toBe(\"ECONNRESET\");\n        maybe_done();\n      });\n\n      proxy.on(\"error\", (err) => {\n        expect(err).toBeInstanceOf(Error);\n        expect((err as any).code).toBe(\"ECONNREFUSED\");\n        proxyServer.close(() => {});\n        maybe_done();\n      });\n      await promise;\n    });\n\n    it(\"should close client socket if upstream is closed before upgrade\", async () => {\n      const { resolve, promise } = Promise.withResolvers<void>();\n\n      const server = http.createServer();\n      server.on(\"upgrade\", function (req, socket, head) {\n        const response = [\"HTTP/1.1 404 Not Found\", \"Content-type: text/html\", \"\", \"\"];\n        socket.write(response.join(\"\\r\\n\"));\n        socket.end();\n      });\n      const sourcePort = await listenOn(server);\n\n      const proxy = httpProxy.createProxyServer({\n        // note: we don't ever listen on this port\n        target: \"ws://127.0.0.1:\" + sourcePort,\n        ws: true,\n      });\n      const proxyPort = await proxyListen(proxy);\n      const proxyServer = proxy;\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n      client.on(\"open\", () => {\n        client.send(\"hello there\");\n      });\n\n      client.on(\"error\", (err) => {\n        expect(err).toBeInstanceOf(Error);\n        proxyServer.close(resolve);\n      });\n\n      await promise;\n    });\n\n    it(\"should not crash when upstream response errors during non-upgrade pipe\", async () => {\n      // Regression: https://github.com/http-party/node-http-proxy/pull/1439\n      const { resolve, promise } = Promise.withResolvers<void>();\n\n      const server = http.createServer((req, res) => {\n        res.writeHead(502);\n        res.write(\"partial\");\n        setTimeout(() => req.socket.destroy(), 10);\n      });\n      const sourcePort = await listenOn(server);\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"ws://127.0.0.1:\" + sourcePort,\n        ws: true,\n      });\n\n      proxy.on(\"error\", () => {\n        // Error handler - the fix ensures this is called instead of crashing\n      });\n\n      const proxyPort = await proxyListen(proxy);\n\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n      client.on(\"error\", () => {});\n      client.on(\"close\", () => {\n        proxy.close(resolve);\n      });\n\n      await promise;\n      server.close();\n    });\n\n    it(\"should proxy a socket.io stream\", async () => {\n      const { resolve, promise } = Promise.withResolvers<void>();\n\n      const server = http.createServer();\n      const sourcePort = await listenOn(server);\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"ws://127.0.0.1:\" + sourcePort,\n        ws: true,\n      });\n      const proxyPort = await proxyListen(proxy);\n      const proxyServer = proxy;\n      const destiny = new io.Server(server);\n\n      function startSocketIo() {\n        const client = ioClient(\"ws://127.0.0.1:\" + proxyPort);\n        client.on(\"connect\", () => {\n          client.emit(\"incoming\", \"hello there\");\n        });\n\n        client.on(\"outgoing\", (data: any) => {\n          expect(data).toBe(\"Hello over websockets\");\n          client.disconnect();\n          destiny.close();\n          server.close();\n          proxyServer.close(resolve);\n        });\n      }\n      startSocketIo();\n\n      destiny.on(\"connection\", (socket) => {\n        socket.on(\"incoming\", (msg) => {\n          expect(msg).toBe(\"hello there\");\n          socket.emit(\"outgoing\", \"Hello over websockets\");\n        });\n      });\n\n      await promise;\n    });\n\n    it(\"should emit open and close events when socket.io client connects and disconnects\", async () => {\n      const { resolve, promise } = Promise.withResolvers<void>();\n\n      const server = http.createServer();\n      const sourcePort = await listenOn(server);\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"ws://127.0.0.1:\" + sourcePort,\n        ws: true,\n      });\n      const proxyPort = await proxyListen(proxy);\n      const proxyServer = proxy;\n      const destiny = new io.Server(server);\n\n      function startSocketIo() {\n        const client = ioClient(\"ws://127.0.0.1:\" + proxyPort);\n        client.on(\"connect\", () => {\n          client.disconnect();\n        });\n      }\n      let count = 0;\n\n      proxyServer.on(\"open\", () => {\n        count += 1;\n      });\n\n      proxyServer.on(\"close\", () => {\n        destiny.close();\n        server.close();\n        proxyServer.close(() => {});\n        expect(count).toBe(1);\n        resolve();\n      });\n\n      startSocketIo();\n      await promise;\n    });\n\n    it(\"should pass all set-cookie headers to client\", async () => {\n      const { resolve, promise } = Promise.withResolvers<void>();\n\n      const destiny = new ws.WebSocketServer({ port: 0 });\n      await new Promise<void>((r) => destiny.on(\"listening\", r));\n      const sourcePort = (destiny.address() as AddressInfo).port;\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"ws://127.0.0.1:\" + sourcePort,\n        ws: true,\n      });\n      const proxyPort = await proxyListen(proxy);\n      const proxyServer = proxy;\n\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n      client.on(\"upgrade\", (res) => {\n        expect(res.headers[\"set-cookie\"]).toHaveLength(2);\n      });\n\n      client.on(\"open\", () => {\n        client.close();\n        destiny.close();\n        proxyServer.close(resolve);\n      });\n\n      destiny.on(\"headers\", (headers) => {\n        headers.push(\"Set-Cookie: test1=test1\", \"Set-Cookie: test2=test2\");\n      });\n\n      await promise;\n    });\n\n    it(\"should detect a proxyReq event and modify headers\", async () => {\n      const { promise, resolve } = Promise.withResolvers<void>();\n\n      const destiny = new ws.WebSocketServer({ port: 0 });\n      await new Promise<void>((r) => destiny.on(\"listening\", r));\n      const sourcePort = (destiny.address() as AddressInfo).port;\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"ws://127.0.0.1:\" + sourcePort,\n        ws: true,\n      });\n\n      proxy.on(\"proxyReqWs\", function (proxyReq, req, socket, options, head) {\n        proxyReq.setHeader(\"X-Special-Proxy-Header\", \"foobar\");\n      });\n\n      const proxyPort = await proxyListen(proxy);\n      const proxyServer = proxy;\n\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n      client.on(\"open\", () => {\n        client.send(\"hello there\");\n      });\n\n      client.on(\"message\", (msg: any) => {\n        expect(msg.toString(\"utf8\")).toBe(\"Hello over websockets\");\n        client.close();\n        destiny.close();\n        proxyServer.close(resolve);\n      });\n\n      destiny.on(\"connection\", function (socket, upgradeReq) {\n        expect(upgradeReq.headers[\"x-special-proxy-header\"]).to.eql(\"foobar\");\n\n        socket.on(\"message\", (msg: any) => {\n          expect(msg.toString(\"utf8\")).toBe(\"hello there\");\n          socket.send(\"Hello over websockets\");\n        });\n      });\n\n      await promise;\n    });\n\n    it(\"should forward frames with single frame payload (including on node 4.x)\", async () => {\n      const { resolve, promise } = await Promise.withResolvers<void>();\n      const payload = Array.from({ length: 65_529 }).join(\"0\");\n\n      const destiny = new ws.WebSocketServer({ port: 0 });\n      await new Promise<void>((r) => destiny.on(\"listening\", r));\n      const sourcePort = (destiny.address() as AddressInfo).port;\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"ws://127.0.0.1:\" + sourcePort,\n        ws: true,\n      });\n      const proxyPort = await proxyListen(proxy);\n      const proxyServer = proxy;\n\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n      client.on(\"open\", () => {\n        client.send(payload);\n      });\n\n      client.on(\"message\", (msg) => {\n        expect(msg.toString(\"utf8\")).toBe(\"Hello over websockets\");\n        client.close();\n        destiny.close();\n        proxyServer.close(resolve);\n      });\n\n      destiny.on(\"connection\", (socket) => {\n        socket.on(\"message\", (msg) => {\n          expect(msg.toString(\"utf8\")).toBe(payload);\n          socket.send(\"Hello over websockets\");\n        });\n      });\n\n      await promise;\n    });\n\n    it(\"should forward continuation frames with big payload (including on node 4.x)\", async () => {\n      const { promise, resolve } = Promise.withResolvers<void>();\n      const payload = Array.from({ length: 65_530 }).join(\"0\");\n\n      const destiny = new ws.WebSocketServer({ port: 0 });\n      await new Promise<void>((r) => destiny.on(\"listening\", r));\n      const sourcePort = (destiny.address() as AddressInfo).port;\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"ws://127.0.0.1:\" + sourcePort,\n        ws: true,\n      });\n      const proxyPort = await proxyListen(proxy);\n      const proxyServer = proxy;\n\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n      client.on(\"open\", () => {\n        client.send(payload);\n      });\n\n      client.on(\"message\", (msg) => {\n        expect(msg.toString(\"utf8\")).toBe(\"Hello over websockets\");\n        client.close();\n        destiny.close();\n        proxyServer.close(resolve);\n      });\n\n      destiny.on(\"connection\", (socket) => {\n        socket.on(\"message\", (msg) => {\n          expect(msg.toString(\"utf8\")).toBe(payload);\n          socket.send(\"Hello over websockets\");\n        });\n      });\n\n      await promise;\n    });\n\n    it(\"should not crash when client socket errors before upstream upgrade (issue #79)\", async () => {\n      const { promise, resolve } = Promise.withResolvers<void>();\n\n      // Backend that delays responding to the upgrade request\n      const server = http.createServer();\n      server.on(\"upgrade\", (_req, socket) => {\n        // Never respond — simulate a slow/hanging backend\n        socket.on(\"error\", () => {});\n        setTimeout(() => socket.destroy(), 500);\n      });\n      const sourcePort = await listenOn(server);\n\n      const proxy = httpProxy.createProxyServer({\n        target: \"ws://127.0.0.1:\" + sourcePort,\n        ws: true,\n      });\n\n      // Intercept the ws stream pass to inject an error on the client socket\n      // before the upstream upgrade response arrives\n      proxy.before(\"ws\", \"\", ((_req: any, socket: any) => {\n        // After the proxy sets up the upstream request but before the\n        // upgrade callback fires, simulate a client disconnect (ECONNRESET)\n        setTimeout(() => {\n          socket.destroy(new Error(\"read ECONNRESET\"));\n        }, 50);\n      }) as any);\n\n      const proxyPort = await proxyListen(proxy);\n\n      proxy.on(\"error\", () => {\n        // The error should be caught here, not crash the process\n        proxy.close(() => {});\n        server.close();\n        resolve();\n      });\n\n      // Use a raw TCP socket to send a WebSocket upgrade request\n      const client = net.connect(proxyPort, \"127.0.0.1\", () => {\n        client.write(\n          \"GET / HTTP/1.1\\r\\n\" +\n            \"Host: 127.0.0.1\\r\\n\" +\n            \"Upgrade: websocket\\r\\n\" +\n            \"Connection: Upgrade\\r\\n\" +\n            \"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\\r\\n\" +\n            \"Sec-WebSocket-Version: 13\\r\\n\" +\n            \"\\r\\n\",\n        );\n      });\n      client.on(\"error\", () => {});\n\n      await promise;\n    });\n  });\n});\n"
  },
  {
    "path": "test/http2-proxy.test.ts",
    "content": "import * as http from \"node:http\";\nimport * as https from \"node:https\";\nimport * as httpProxy from \"../src/index.ts\";\nimport * as path from \"node:path\";\nimport * as fs from \"node:fs\";\nimport { describe, it, expect, afterAll, beforeAll } from \"vitest\";\n\nimport { Agent, fetch } from \"undici\";\nimport { listenOn, proxyListen } from \"./_utils.ts\";\n\nimport { inspect } from \"node:util\";\n\nconst http1Agent = new Agent({\n  allowH2: false,\n  connect: {\n    // Allow to use SSL self signed\n    rejectUnauthorized: false,\n  },\n});\nconst http2Agent = new Agent({\n  allowH2: true,\n  connect: {\n    // Allow to use SSL self signed\n    rejectUnauthorized: false,\n  },\n});\n\ndescribe(\"http/2 listener\", () => {\n  describe(\"http2 -> http\", () => {\n    let source: http.Server;\n    let sourcePort: number;\n    let proxy: httpProxy.ProxyServer;\n    let proxyPort: number;\n\n    beforeAll(async () => {\n      source = http.createServer((_req, res) => {\n        res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n        res.end(\"Hello from \" + sourcePort);\n      });\n      sourcePort = await listenOn(source);\n\n      proxy = httpProxy.createProxyServer({\n        target: \"http://127.0.0.1:\" + sourcePort,\n        ssl: {\n          key: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-key.pem\")),\n          cert: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-cert.pem\")),\n        },\n        http2: true,\n        // Allow to use SSL self signed\n        secure: false,\n      });\n      proxyPort = await proxyListen(proxy);\n    });\n\n    it(\"target http server should be working\", async () => {\n      try {\n        const r = await (\n          await fetch(`http://127.0.0.1:${sourcePort}`, { dispatcher: http1Agent })\n        ).text();\n        expect(r).to.eql(\"Hello from \" + sourcePort);\n      } catch (err) {\n        expect.fail(\"Failed to fetch target server: \" + inspect(err));\n      }\n    });\n\n    it(\"fetch proxy server over http1\", async () => {\n      try {\n        const r = await (\n          await fetch(`https://127.0.0.1:${proxyPort}`, { dispatcher: http1Agent })\n        ).text();\n        expect(r).to.eql(\"Hello from \" + sourcePort);\n      } catch (err) {\n        expect.fail(\"Failed to fetch target server: \" + inspect(err));\n      }\n    });\n\n    it(\"fetch proxy server over http2\", async () => {\n      try {\n        const resp = await fetch(`https://127.0.0.1:${proxyPort}`, { dispatcher: http2Agent });\n        const r = await resp.text();\n        expect(r).to.eql(\"Hello from \" + sourcePort);\n      } catch (err) {\n        expect.fail(\"Failed to fetch target server: \" + inspect(err));\n      }\n    });\n\n    afterAll(async () => {\n      // cleans up\n      await new Promise<void>((resolve) => proxy.close(resolve));\n      source.close();\n    });\n  });\n\n  describe(\"http2 -> https\", () => {\n    let source: https.Server;\n    let sourcePort: number;\n    let proxy: httpProxy.ProxyServer;\n    let proxyPort: number;\n\n    beforeAll(async () => {\n      source = https.createServer(\n        {\n          key: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-key.pem\")),\n          cert: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-cert.pem\")),\n          ciphers: \"AES128-GCM-SHA256\",\n        },\n        function (req, res) {\n          res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n          res.end(\"Hello from \" + sourcePort);\n        },\n      );\n\n      sourcePort = await listenOn(source);\n\n      proxy = httpProxy.createProxyServer({\n        target: \"https://127.0.0.1:\" + sourcePort,\n        ssl: {\n          key: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-key.pem\")),\n          cert: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-cert.pem\")),\n        },\n        http2: true,\n        // Allow to use SSL self signed\n        secure: false,\n      });\n\n      proxyPort = await proxyListen(proxy);\n    });\n\n    it(\"target https server should be working\", async () => {\n      try {\n        const r = await (\n          await fetch(`https://127.0.0.1:${sourcePort}`, { dispatcher: http1Agent })\n        ).text();\n        expect(r).to.eql(\"Hello from \" + sourcePort);\n      } catch (err) {\n        expect.fail(\"Failed to fetch target server: \" + inspect(err));\n      }\n    });\n\n    it(\"fetch proxy server over http1\", async () => {\n      try {\n        const r = await (\n          await fetch(`https://127.0.0.1:${proxyPort}`, { dispatcher: http1Agent })\n        ).text();\n        expect(r).to.eql(\"Hello from \" + sourcePort);\n      } catch (err) {\n        expect.fail(\"Failed to fetch target server: \" + inspect(err));\n      }\n    });\n\n    it(\"fetch proxy server over http2\", async () => {\n      try {\n        const resp = await fetch(`https://127.0.0.1:${proxyPort}`, { dispatcher: http2Agent });\n        const r = await resp.text();\n        expect(r).to.eql(\"Hello from \" + sourcePort);\n      } catch (err) {\n        expect.fail(\"Failed to fetch target server: \" + inspect(err));\n      }\n    });\n\n    afterAll(async () => {\n      // cleans up\n      await new Promise<void>((resolve) => proxy.close(resolve));\n      source.close();\n    });\n  });\n});\n"
  },
  {
    "path": "test/https-proxy.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport * as httpProxy from \"../src/index.ts\";\nimport http from \"node:http\";\nimport https from \"node:https\";\nimport path from \"node:path\";\nimport fs from \"node:fs\";\nimport { listenOn, proxyListen } from \"./_utils.ts\";\n\n// Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-https-proxy-test.js\n\ndescribe(\"lib/http-proxy.js\", () => {\n  describe(\"HTTPS #createProxyServer\", () => {\n    describe(\"HTTPS to HTTP\", () => {\n      it(\"should proxy the request en send back the response\", async () => {\n        const { promise, resolve } = Promise.withResolvers<void>();\n\n        const source = http.createServer(function (req, res) {\n          expect(req.method).to.eql(\"GET\");\n          expect(Number.parseInt(req.headers.host!.split(\":\")[1]!)).to.eql(proxyPort);\n          res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n          res.end(\"Hello from \" + sourcePort);\n        });\n        const sourcePort = await listenOn(source);\n\n        const proxy = httpProxy.createProxyServer({\n          target: \"http://127.0.0.1:\" + sourcePort,\n          ssl: {\n            key: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-key.pem\")),\n            cert: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-cert.pem\")),\n            ciphers: \"AES128-GCM-SHA256\",\n          },\n        });\n        const proxyPort = await proxyListen(proxy);\n\n        https\n          .request(\n            {\n              host: \"127.0.0.1\",\n              port: proxyPort,\n              path: \"/\",\n              method: \"GET\",\n              rejectUnauthorized: false,\n            },\n            function (res) {\n              expect(res.statusCode).to.eql(200);\n\n              res.on(\"data\", function (data) {\n                expect(data.toString()).to.eql(\"Hello from \" + sourcePort);\n              });\n\n              res.on(\"end\", () => {\n                source.close();\n                proxy.close(resolve);\n              });\n            },\n          )\n          .end();\n\n        await promise;\n      });\n    });\n\n    describe(\"HTTP to HTTPS\", () => {\n      it(\"should proxy the request en send back the response\", async () => {\n        const { resolve, promise } = Promise.withResolvers<void>();\n\n        const source = https.createServer(\n          {\n            key: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-key.pem\")),\n            cert: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-cert.pem\")),\n            ciphers: \"AES128-GCM-SHA256\",\n          },\n          function (req, res) {\n            expect(req.method).to.eql(\"GET\");\n            expect(Number.parseInt(req.headers.host!.split(\":\")[1]!)).to.eql(proxyPort);\n            res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n            res.end(\"Hello from \" + sourcePort);\n          },\n        );\n        const sourcePort = await listenOn(source);\n\n        const proxy = httpProxy.createProxyServer({\n          target: \"https://127.0.0.1:\" + sourcePort,\n          // Allow to use SSL self signed\n          secure: false,\n        });\n        const proxyPort = await proxyListen(proxy);\n\n        http\n          .request(\n            {\n              hostname: \"127.0.0.1\",\n              port: proxyPort,\n              method: \"GET\",\n            },\n            function (res) {\n              expect(res.statusCode).to.eql(200);\n\n              res.on(\"data\", function (data) {\n                expect(data.toString()).to.eql(\"Hello from \" + sourcePort);\n              });\n\n              res.on(\"end\", () => {\n                source.close();\n                proxy.close(resolve);\n              });\n            },\n          )\n          .end();\n        await promise;\n      });\n    });\n\n    describe(\"HTTPS to HTTPS\", () => {\n      it(\"should proxy the request en send back the response\", async () => {\n        const { resolve, promise } = Promise.withResolvers<void>();\n\n        const source = https.createServer(\n          {\n            key: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-key.pem\")),\n            cert: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-cert.pem\")),\n            ciphers: \"AES128-GCM-SHA256\",\n          },\n          function (req, res) {\n            expect(req.method).to.eql(\"GET\");\n            expect(Number.parseInt(req.headers.host!.split(\":\")[1]!)).to.eql(proxyPort);\n            res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n            res.end(\"Hello from \" + sourcePort);\n          },\n        );\n        const sourcePort = await listenOn(source);\n\n        const proxy = httpProxy.createProxyServer({\n          target: \"https://127.0.0.1:\" + sourcePort,\n          ssl: {\n            key: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-key.pem\")),\n            cert: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-cert.pem\")),\n            ciphers: \"AES128-GCM-SHA256\",\n          },\n          secure: false,\n        });\n        const proxyPort = await proxyListen(proxy);\n\n        https\n          .request(\n            {\n              host: \"127.0.0.1\",\n              port: proxyPort,\n              path: \"/\",\n              method: \"GET\",\n              rejectUnauthorized: false,\n            },\n            function (res) {\n              expect(res.statusCode).to.eql(200);\n\n              res.on(\"data\", function (data) {\n                expect(data.toString()).to.eql(\"Hello from \" + sourcePort);\n              });\n\n              res.on(\"end\", () => {\n                source.close();\n                proxy.close(resolve);\n              });\n            },\n          )\n          .end();\n        await promise;\n      });\n    });\n\n    describe(\"HTTPS not allow SSL self signed\", () => {\n      it(\"should fail with error\", async () => {\n        const { resolve, promise } = Promise.withResolvers<void>();\n\n        const source = https.createServer({\n          key: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-key.pem\")),\n          cert: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-cert.pem\")),\n          ciphers: \"AES128-GCM-SHA256\",\n        });\n        const sourcePort = await listenOn(source);\n\n        const proxy = httpProxy.createProxyServer({\n          target: \"https://127.0.0.1:\" + sourcePort,\n          secure: true,\n        });\n        const proxyPort = await proxyListen(proxy);\n\n        proxy.on(\"error\", function (err) {\n          expect(err).toBeInstanceOf(Error);\n          expect(err.toString()).toMatch(\n            /unable to verify the first certificate|DEPTH_ZERO_SELF_SIGNED_CERT/,\n          );\n          source.close();\n          proxy.close();\n          resolve();\n        });\n\n        http\n          .request({\n            hostname: \"127.0.0.1\",\n            port: proxyPort,\n            method: \"GET\",\n          })\n          .end();\n\n        await promise;\n      });\n    });\n\n    describe(\"HTTPS to HTTP using own server\", () => {\n      it(\"should proxy the request en send back the response\", async () => {\n        const { resolve, promise } = Promise.withResolvers<void>();\n\n        const source = http.createServer(function (req, res) {\n          expect(req.method).to.eql(\"GET\");\n          expect(Number.parseInt(req.headers.host!.split(\":\")[1]!)).to.eql(proxyPort);\n          res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n          res.end(\"Hello from \" + sourcePort);\n        });\n        const sourcePort = await listenOn(source);\n\n        const proxy = httpProxy.createProxyServer({\n          agent: new http.Agent({ maxSockets: 2 }),\n        });\n\n        const ownServer = https.createServer(\n          {\n            key: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-key.pem\")),\n            cert: fs.readFileSync(path.join(__dirname, \"fixtures\", \"agent2-cert.pem\")),\n            ciphers: \"AES128-GCM-SHA256\",\n          },\n          function (req, res) {\n            proxy.web(req, res, {\n              target: \"http://127.0.0.1:\" + sourcePort,\n            });\n          },\n        );\n        const proxyPort = await listenOn(ownServer);\n\n        https\n          .request(\n            {\n              host: \"127.0.0.1\",\n              port: proxyPort,\n              path: \"/\",\n              method: \"GET\",\n              rejectUnauthorized: false,\n            },\n            function (res) {\n              expect(res.statusCode).to.eql(200);\n\n              res.on(\"data\", function (data) {\n                expect(data.toString()).to.eql(\"Hello from \" + sourcePort);\n              });\n\n              res.on(\"end\", () => {\n                source.close();\n                ownServer.close();\n                resolve();\n              });\n            },\n          )\n          .end();\n\n        await promise;\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/index.test.ts",
    "content": "import { afterAll, describe, expect, it } from \"vitest\";\nimport http, { type IncomingMessage, type ServerResponse } from \"node:http\";\nimport { type AddressInfo } from \"node:net\";\nimport { $fetch } from \"ofetch\";\nimport { createProxyServer, ProxyServer, type ProxyServerOptions } from \"../src/index.ts\";\n\ntype Listener = {\n  close: () => Promise<void>;\n  url: string;\n};\n\nfunction listen(handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>) {\n  return new Promise<Listener>((resolve, reject) => {\n    const server = http.createServer((req, res) => {\n      void handler(req, res);\n    });\n\n    server.once(\"error\", reject);\n    server.listen(0, \"127.0.0.1\", () => {\n      const { port } = server.address() as AddressInfo;\n      resolve({\n        close: () =>\n          new Promise<void>((resolveClose, rejectClose) => {\n            server.close((error) => {\n              if (error) {\n                rejectClose(error);\n                return;\n              }\n              resolveClose();\n            });\n          }),\n        url: `http://127.0.0.1:${port}/`,\n      });\n    });\n  });\n}\n\ndescribe(\"httpxy\", () => {\n  let mainListener: Listener;\n  let proxyListener: Listener;\n  let proxy: ProxyServer;\n\n  let lastResolved: any;\n  let lastRejected: any;\n\n  const maskResponse = (obj: any) => ({\n    ...obj,\n    headers: { ...obj.headers, connection: \"<>\", host: \"<>\" },\n  });\n\n  const makeProxy = async (options: ProxyServerOptions) => {\n    mainListener = await listen((req, res) => {\n      res.end(\n        JSON.stringify({\n          method: req.method,\n          path: req.url,\n          headers: req.headers,\n        }),\n      );\n    });\n\n    proxy = createProxyServer(options);\n\n    proxyListener = await listen(async (req, res) => {\n      lastResolved = false;\n      lastRejected = undefined;\n      try {\n        await proxy.web(req, res, { target: mainListener.url + \"base\" });\n        lastResolved = true;\n      } catch (error) {\n        lastRejected = error;\n        res.statusCode = 500;\n        res.end(\"Proxy error: \" + (error as Error).toString());\n      }\n    });\n  };\n\n  afterAll(async () => {\n    await proxyListener?.close();\n    await mainListener?.close();\n    proxy?.close();\n  });\n\n  it(\"works\", async () => {\n    await makeProxy({});\n    const mainResponse = await $fetch(mainListener.url + \"base/test?foo\");\n    const proxyResponse = await $fetch(proxyListener.url + \"test?foo\");\n\n    expect(maskResponse(await mainResponse)).toMatchObject(maskResponse(proxyResponse));\n\n    expect(proxyResponse.path).toBe(\"/base/test?foo\");\n\n    expect(lastResolved).toBe(true);\n    expect(lastRejected).toBe(undefined);\n  });\n\n  it(\"should avoid normalize url\", async () => {\n    const mainResponse = await $fetch(mainListener.url + \"base/a/b//c\");\n    const proxyResponse = await $fetch(proxyListener.url + \"a/b//c\");\n\n    expect(maskResponse(await mainResponse)).toMatchObject(maskResponse(proxyResponse));\n\n    expect(proxyResponse.path).toBe(\"/base/a/b//c\");\n\n    expect(lastResolved).toBe(true);\n    expect(lastRejected).toBe(undefined);\n  });\n});\n\ndescribe(\"middleware pass exceptions\", () => {\n  it(\"should forward synchronous pass errors to error event\", async () => {\n    const target = await new Promise<{\n      close: () => Promise<void>;\n      url: string;\n    }>((resolve, reject) => {\n      const server = http.createServer((_req, res) => {\n        res.end(\"ok\");\n      });\n      server.once(\"error\", reject);\n      server.listen(0, \"127.0.0.1\", () => {\n        const { port } = server.address() as AddressInfo;\n        resolve({\n          close: () =>\n            new Promise<void>((r, j) => {\n              server.close((e) => (e ? j(e) : r()));\n            }),\n          url: `http://127.0.0.1:${port}/`,\n        });\n      });\n    });\n\n    const proxy = createProxyServer({ target: target.url });\n\n    // Inject a middleware pass that throws synchronously (simulates ERR_INVALID_HTTP_TOKEN)\n    const testError = new TypeError(\"Invalid character in header\");\n    proxy.before(\"web\", \"\", () => {\n      throw testError;\n    });\n\n    const proxyServer = await new Promise<{\n      close: () => Promise<void>;\n      url: string;\n    }>((resolve, reject) => {\n      const server = http.createServer((req, res) => {\n        void proxy.web(req, res);\n      });\n      server.once(\"error\", reject);\n      server.listen(0, \"127.0.0.1\", () => {\n        const { port } = server.address() as AddressInfo;\n        resolve({\n          close: () =>\n            new Promise<void>((r, j) => {\n              server.close((e) => (e ? j(e) : r()));\n            }),\n          url: `http://127.0.0.1:${port}/`,\n        });\n      });\n    });\n\n    try {\n      // With an error listener, the error should be emitted, not thrown\n      const errorPromise = new Promise<Error>((resolve) => {\n        proxy.on(\"error\", (err, _req, res) => {\n          resolve(err);\n          // End the response so the request doesn't hang\n          if (res && \"writeHead\" in res && !res.headersSent) {\n            res.writeHead(502);\n            res.end();\n          }\n        });\n      });\n\n      // The request may fail since the proxy errored before sending a response\n      await $fetch(proxyServer.url, { ignoreResponseError: true }).catch(() => {});\n\n      const emittedError = await errorPromise;\n      expect(emittedError).toBe(testError);\n    } finally {\n      proxy.close();\n      await proxyServer.close();\n      await target.close();\n    }\n  });\n\n  it(\"should reject promise when no error listener and pass throws\", async () => {\n    const target = await new Promise<{\n      close: () => Promise<void>;\n      url: string;\n    }>((resolve, reject) => {\n      const server = http.createServer((_req, res) => {\n        res.end(\"ok\");\n      });\n      server.once(\"error\", reject);\n      server.listen(0, \"127.0.0.1\", () => {\n        const { port } = server.address() as AddressInfo;\n        resolve({\n          close: () =>\n            new Promise<void>((r, j) => {\n              server.close((e) => (e ? j(e) : r()));\n            }),\n          url: `http://127.0.0.1:${port}/`,\n        });\n      });\n    });\n\n    const proxy = createProxyServer({ target: target.url });\n\n    // Inject a middleware pass that throws synchronously\n    const testError = new TypeError(\"Invalid character in header\");\n    proxy.before(\"web\", \"\", () => {\n      throw testError;\n    });\n\n    const proxyServer = await new Promise<{\n      close: () => Promise<void>;\n      url: string;\n    }>((resolve, reject) => {\n      const server = http.createServer((req, res) => {\n        void proxy.web(req, res).catch(() => {\n          res.statusCode = 502;\n          res.end(\"error\");\n        });\n      });\n      server.once(\"error\", reject);\n      server.listen(0, \"127.0.0.1\", () => {\n        const { port } = server.address() as AddressInfo;\n        resolve({\n          close: () =>\n            new Promise<void>((r, j) => {\n              server.close((e) => (e ? j(e) : r()));\n            }),\n          url: `http://127.0.0.1:${port}/`,\n        });\n      });\n    });\n\n    try {\n      // No error listener - the promise should reject with the thrown error\n      const response = await $fetch.raw(proxyServer.url, {\n        ignoreResponseError: true,\n      });\n      expect(response.status).toBe(502);\n    } finally {\n      proxy.close();\n      await proxyServer.close();\n      await target.close();\n    }\n  });\n});\n"
  },
  {
    "path": "test/middleware/web-incoming.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\n\nimport * as webPasses from \"../../src/middleware/web-incoming.ts\";\nimport * as httpProxy from \"../../src/index.ts\";\nimport concat from \"concat-stream\";\nimport http from \"node:http\";\nimport {\n  stubIncomingMessage,\n  stubServerResponse,\n  stubMiddlewareOptions,\n  stubProxyServer,\n} from \"../_stubs.ts\";\nimport { listenOn, proxyListen } from \"../_utils.ts\";\n\n// Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-http-proxy-passes-web-incoming-test.js\n\ndescribe(\"middleware:web-incoming\", () => {\n  describe(\"#deleteLength\", () => {\n    it(\"should change `content-length` for DELETE requests\", () => {\n      const stubRequest = stubIncomingMessage({\n        method: \"DELETE\",\n        headers: {},\n      });\n      webPasses.deleteLength(\n        stubRequest,\n        stubServerResponse(),\n        stubMiddlewareOptions(),\n        stubProxyServer(),\n      );\n      expect(stubRequest.headers[\"content-length\"]).to.eql(\"0\");\n    });\n\n    it(\"should change `content-length` for OPTIONS requests\", () => {\n      const stubRequest = stubIncomingMessage({\n        method: \"OPTIONS\",\n        headers: {},\n      });\n      webPasses.deleteLength(\n        stubRequest,\n        stubServerResponse(),\n        stubMiddlewareOptions(),\n        stubProxyServer(),\n      );\n      expect(stubRequest.headers[\"content-length\"]).to.eql(\"0\");\n    });\n\n    it(\"should remove `transfer-encoding` from empty DELETE requests\", () => {\n      const stubRequest = stubIncomingMessage({\n        method: \"DELETE\",\n        headers: {\n          \"transfer-encoding\": \"chunked\",\n        },\n      });\n      webPasses.deleteLength(\n        stubRequest,\n        stubServerResponse(),\n        stubMiddlewareOptions(),\n        stubProxyServer(),\n      );\n      expect(stubRequest.headers[\"content-length\"]).to.eql(\"0\");\n      expect(stubRequest.headers).to.not.have.key(\"transfer-encoding\");\n    });\n  });\n\n  describe(\"#timeout\", () => {\n    it(\"should set timeout on the socket\", () => {\n      let done = false;\n      const stubRequest = stubIncomingMessage({\n        socket: {\n          setTimeout: function (value: any) {\n            done = value;\n          },\n        },\n      });\n\n      webPasses.timeout(\n        stubRequest,\n        stubServerResponse(),\n        stubMiddlewareOptions({ timeout: 5000 }),\n        stubProxyServer(),\n      );\n      expect(done).to.eql(5000);\n    });\n  });\n\n  describe(\"#XHeaders\", () => {\n    const req = stubIncomingMessage({\n      connection: {\n        remoteAddress: \"192.168.1.2\",\n        remotePort: \"8080\",\n      },\n      headers: {\n        host: \"192.168.1.2:8080\",\n      },\n    });\n\n    it(\"set the correct x-forwarded-* headers\", () => {\n      webPasses.XHeaders(\n        req,\n        stubServerResponse(),\n        stubMiddlewareOptions({ xfwd: true }),\n        stubProxyServer(),\n      );\n      expect(req.headers[\"x-forwarded-for\"]).toBe(\"192.168.1.2\");\n      expect(req.headers[\"x-forwarded-port\"]).toBe(\"8080\");\n      expect(req.headers[\"x-forwarded-proto\"]).toBe(\"http\");\n    });\n\n    it(\"should not overwrite existing x-forwarded-* headers\", () => {\n      const stubRequest = stubIncomingMessage({\n        connection: {\n          remoteAddress: \"192.168.1.2\",\n          remotePort: \"8080\",\n        },\n        headers: {\n          host: \"192.168.1.2:8080\",\n          \"x-forwarded-host\": \"192.168.1.3:8081\",\n          \"x-forwarded-for\": \"192.168.1.3\",\n          \"x-forwarded-port\": \"8081\",\n          \"x-forwarded-proto\": \"https\",\n        },\n      });\n      webPasses.XHeaders(\n        stubRequest,\n        stubServerResponse(),\n        stubMiddlewareOptions({ xfwd: true }),\n        stubProxyServer(),\n      );\n      expect(stubRequest.headers[\"x-forwarded-for\"]).toBe(\"192.168.1.3\");\n      expect(stubRequest.headers[\"x-forwarded-port\"]).toBe(\"8081\");\n      expect(stubRequest.headers[\"x-forwarded-proto\"]).toBe(\"https\");\n      expect(stubRequest.headers[\"x-forwarded-host\"]).toBe(\"192.168.1.3:8081\");\n    });\n  });\n});\n\ndescribe(\"#stream middleware direct tests\", () => {\n  it(\"should emit error on server when callback is not provided\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    const EventEmitter = (await import(\"node:events\")).EventEmitter;\n    const server = Object.assign(new EventEmitter(), {\n      _webPasses: [],\n      _wsPasses: [],\n    }) as any;\n\n    server.on(\"error\", (err: Error) => {\n      expect(err).toBeInstanceOf(Error);\n      resolve();\n    });\n\n    // Call stream directly without callback (6th arg)\n    const stubReq = Object.assign(new (await import(\"node:stream\")).PassThrough(), {\n      method: \"GET\",\n      url: \"/\",\n      headers: { host: \"127.0.0.1\" },\n      connection: { remoteAddress: \"127.0.0.1\" },\n      socket: { remoteAddress: \"127.0.0.1\", destroyed: false },\n    });\n    const stubRes = Object.assign(new (await import(\"node:stream\")).PassThrough(), {\n      headersSent: false,\n      finished: false,\n      setHeader: () => {},\n      writeHead: () => {},\n      statusCode: 200,\n    });\n\n    webPasses.stream(\n      stubReq as any,\n      stubRes as any,\n      { target: new URL(`http://127.0.0.1:54322`), forward: undefined as any } as any,\n      server as any,\n      undefined,\n      // No callback - this will trigger line 131\n      undefined,\n    );\n\n    await promise;\n  });\n\n  it(\"should emit end event when res.finished is true\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n    const source = http.createServer((_req, res) => {\n      res.end(\"done\");\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n      selfHandleResponse: true,\n    });\n\n    const proxyServer = http.createServer((req, res) => {\n      proxy.once(\"proxyRes\", (_proxyRes, _pReq, pRes) => {\n        // End the response before proxyRes piping would happen\n        pRes.end(\"early-end\");\n      });\n\n      proxy.once(\"end\", () => {\n        source.close();\n        proxyServer.close();\n        resolve();\n      });\n\n      proxy.web(req, res);\n    });\n\n    const proxyPort = await listenOn(proxyServer);\n    http.request(`http://127.0.0.1:${proxyPort}/`, () => {}).end();\n    await promise;\n  });\n});\n\ndescribe(\"#stream POST body piping\", () => {\n  it(\"should deliver the full POST body to the target server\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    const postBody = \"x\".repeat(8192); // large enough to test chunked piping\n\n    const source = http.createServer((req, res) => {\n      let body = \"\";\n      req.on(\"data\", (chunk: Buffer) => (body += chunk));\n      req.on(\"end\", () => {\n        res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n        res.end(body);\n      });\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n    });\n\n    const proxyServer = http.createServer((req, res) => {\n      proxy.web(req, res);\n    });\n    const proxyPort = await listenOn(proxyServer);\n\n    http\n      .request(`http://127.0.0.1:${proxyPort}`, { method: \"POST\" }, function (res) {\n        let body = \"\";\n        res.on(\"data\", (chunk: Buffer) => (body += chunk));\n        res.on(\"end\", () => {\n          source.close();\n          proxyServer.close();\n          expect(body).to.eql(postBody);\n          resolve();\n        });\n      })\n      .end(postBody);\n    await promise;\n  });\n});\n\ndescribe(\"#createProxyServer.web() using own http server\", () => {\n  it(\"should proxy the request using the web proxy handler\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    const source = http.createServer(function (req: any, res: any) {\n      source.close();\n      proxyServer.close();\n      expect(req.method).to.eql(\"GET\");\n      expect(Number.parseInt(req.headers.host!.split(\":\")[1])).to.eql(proxyPort);\n      resolve();\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n    });\n\n    function requestHandler(req: any, res: any) {\n      proxy.web(req, res);\n    }\n\n    const proxyServer = http.createServer(requestHandler);\n    const proxyPort = await listenOn(proxyServer);\n\n    http.request(`http://127.0.0.1:${proxyPort}`, () => {}).end();\n\n    await promise;\n  });\n\n  it(\"should detect a proxyReq event and modify headers\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    const source = http.createServer(function (req: any, res: any) {\n      source.close();\n      proxyServer.close();\n      expect(req.headers[\"x-special-proxy-header\"]).to.eql(\"foobar\");\n      resolve();\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n    });\n\n    proxy.on(\"proxyReq\", function (proxyReq, req, res, options) {\n      proxyReq.setHeader(\"X-Special-Proxy-Header\", \"foobar\");\n    });\n\n    function requestHandler(req: any, res: any) {\n      proxy.web(req, res);\n    }\n\n    const proxyServer = http.createServer(requestHandler);\n    const proxyPort = await listenOn(proxyServer);\n\n    http.request(`http://127.0.0.1:${proxyPort}`, () => {}).end();\n    await promise;\n  });\n\n  it('should skip proxyReq event when handling a request with header \"expect: 100-continue\" [https://www.npmjs.com/advisories/1486]', async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    const source = http.createServer(function (req: any, res: any) {\n      source.close();\n      proxyServer.close();\n      expect(req.headers[\"x-special-proxy-header\"]).to.not.eql(\"foobar\");\n      resolve();\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n    });\n\n    proxy.on(\"proxyReq\", function (proxyReq, req, res, options) {\n      proxyReq.setHeader(\"X-Special-Proxy-Header\", \"foobar\");\n    });\n\n    function requestHandler(req: any, res: any) {\n      proxy.web(req, res);\n    }\n\n    const proxyServer = http.createServer(requestHandler);\n    const proxyPort = await listenOn(proxyServer);\n\n    const postData = \"\".padStart(1025, \"x\");\n\n    const postOptions = {\n      hostname: \"127.0.0.1\",\n      port: proxyPort,\n      path: \"/\",\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/x-www-form-urlencoded\",\n        \"Content-Length\": Buffer.byteLength(postData),\n        expect: \"100-continue\",\n      },\n    };\n\n    const req = http.request(postOptions, () => {});\n    req.write(postData);\n    req.end();\n\n    await promise;\n  });\n\n  it(\"should proxy the request and handle error via callback\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n    const proxy = httpProxy.createProxyServer({\n      target: \"http://127.0.0.1:1\",\n    });\n\n    const proxyServer = http.createServer(requestHandler);\n\n    async function requestHandler(req: any, res: any) {\n      const proxyRes = await proxy.web(req, res).catch((error_) => error_);\n      proxyServer.close();\n      resolve();\n      expect(proxyRes).toBeInstanceOf(Error);\n      expect((proxyRes as any).code).toBe(\"ECONNREFUSED\");\n    }\n\n    const proxyPort = await listenOn(proxyServer);\n\n    http\n      .request(\n        {\n          hostname: \"127.0.0.1\",\n          port: proxyPort,\n          method: \"GET\",\n        },\n        () => {},\n      )\n      .end();\n    await promise;\n  });\n\n  it(\"should proxy the request and handle error via event listener\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n    const proxy = httpProxy.createProxyServer({\n      target: \"http://127.0.0.1:54320\",\n    });\n\n    const proxyServer = http.createServer(requestHandler);\n\n    function requestHandler(req: any, res: any) {\n      proxy.once(\"error\", function (err, errReq, errRes) {\n        proxyServer.close();\n        expect(err).toBeInstanceOf(Error);\n        expect(errReq).toBe(req);\n        expect(errRes).toBe(res);\n        expect((err as any).code).toBe(\"ECONNREFUSED\");\n        res.end();\n        resolve();\n      });\n\n      proxy.web(req, res);\n    }\n\n    const proxyPort = await listenOn(proxyServer);\n    http.request({ hostname: \"127.0.0.1\", port: proxyPort, method: \"GET\" }, () => {}).end();\n    await promise;\n  });\n\n  it(\"should forward the request and handle error via event listener\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n    const proxy = httpProxy.createProxyServer({\n      forward: \"http://127.0.0.1:54321\",\n    });\n\n    const proxyServer = http.createServer(requestHandler);\n\n    function requestHandler(req: any, res: any) {\n      proxy.once(\"error\", function (err, errReq, errRes) {\n        proxyServer.close();\n        expect(err).toBeInstanceOf(Error);\n        expect((err as any).code).toBe(\"ECONNREFUSED\");\n        res.end();\n        resolve();\n      });\n\n      proxy.web(req, res);\n    }\n\n    const proxyPort = await listenOn(proxyServer);\n    http.request({ hostname: \"127.0.0.1\", port: proxyPort, method: \"GET\" }, () => {}).end();\n    await promise;\n  });\n\n  it(\"should proxy the request and handle timeout error (proxyTimeout)\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n    const net = await import(\"node:net\");\n\n    // Create a TCP server that accepts but never responds\n    const blackhole = net.createServer((_socket) => {});\n\n    await new Promise<void>((r) => blackhole.listen(0, \"127.0.0.1\", r));\n    const blackholePort = (blackhole.address() as any).port;\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${blackholePort}`,\n      proxyTimeout: 100,\n    });\n\n    const proxyServer = http.createServer(requestHandler);\n\n    const started = Date.now();\n    function requestHandler(req: any, res: any) {\n      proxy.once(\"error\", function (err, errReq, errRes) {\n        proxyServer.close();\n        blackhole.close();\n        expect(err).toBeInstanceOf(Error);\n        expect(errReq).toBe(req);\n        expect(errRes).toBe(res);\n        expect(Date.now() - started).toBeGreaterThan(99);\n        expect((err as any).code).toBe(\"ECONNRESET\");\n        res.end();\n        resolve();\n      });\n\n      proxy.web(req, res);\n    }\n\n    const proxyPort = await listenOn(proxyServer);\n    http.request({ hostname: \"127.0.0.1\", port: proxyPort, method: \"GET\" }, () => {}).end();\n    await promise;\n  });\n\n  // Note: req.on(\"aborted\") no longer fires reliably on Node.js v18+\n  it.todo(\"should proxy the request and handle timeout error\");\n\n  it(\"should proxy the request and provide a proxyRes event with the request and response parameters\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    const source = http.createServer(function (req: any, res: any) {\n      res.end(\"Response\");\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n    });\n\n    function requestHandler(req: any, res: any) {\n      proxy.once(\"proxyRes\", function (proxyRes, pReq, pRes) {\n        source.close();\n        proxyServer.close();\n        expect(pReq).toBe(req);\n        expect(pRes).toBe(res);\n        resolve();\n      });\n\n      proxy.web(req, res);\n    }\n\n    const proxyServer = http.createServer(requestHandler);\n    const proxyPort = await listenOn(proxyServer);\n\n    http.request(`http://127.0.0.1:${proxyPort}`, () => {}).end();\n    await promise;\n  });\n\n  it(\"should proxy the request and provide and respond to manual user response when using modifyResponse\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    const source = http.createServer(function (req: any, res: any) {\n      res.end(\"Response\");\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n      selfHandleResponse: true,\n    });\n\n    function requestHandler(req: any, res: any) {\n      proxy.once(\"proxyRes\", function (proxyRes, pReq, pRes) {\n        proxyRes.pipe(\n          concat(function (body) {\n            expect(body.toString(\"utf8\")).eql(\"Response\");\n            pRes.end(Buffer.from(\"my-custom-response\"));\n          }),\n        );\n      });\n\n      proxy.web(req, res);\n    }\n\n    const proxyServer = http.createServer(requestHandler);\n    const proxyPort = await listenOn(proxyServer);\n\n    http\n      .get(`http://127.0.0.1:${proxyPort}`, function (res) {\n        res.pipe(\n          concat(function (body) {\n            expect(body.toString(\"utf8\")).eql(\"my-custom-response\");\n            source.close();\n            proxyServer.close();\n            resolve();\n          }),\n        );\n      })\n      .once(\"error\", resolve);\n    await promise;\n  });\n\n  it(\"should proxy the request and handle changeOrigin option\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    const source = http.createServer(function (req: any, res: any) {\n      source.close();\n      proxyServer.close();\n      expect(req.method).to.eql(\"GET\");\n      expect(Number.parseInt(req.headers.host!.split(\":\")[1])).to.eql(sourcePort);\n      resolve();\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n      changeOrigin: true,\n    });\n\n    function requestHandler(req: any, res: any) {\n      proxy.web(req, res);\n    }\n\n    const proxyServer = http.createServer(requestHandler);\n    const proxyPort = await listenOn(proxyServer);\n\n    http.request(`http://127.0.0.1:${proxyPort}`, () => {}).end();\n    await promise;\n  });\n\n  it(\"should proxy the request with the Authorization header set\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    const source = http.createServer(function (req: any, res: any) {\n      source.close();\n      proxyServer.close();\n      const auth = Buffer.from(req.headers.authorization.split(\" \")[1], \"base64\");\n      expect(req.method).to.eql(\"GET\");\n      expect(auth.toString()).to.eql(\"user:pass\");\n      resolve();\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n      auth: \"user:pass\",\n    });\n\n    function requestHandler(req: any, res: any) {\n      proxy.web(req, res);\n    }\n\n    const proxyServer = http.createServer(requestHandler);\n    const proxyPort = await listenOn(proxyServer);\n\n    http.request(`http://127.0.0.1:${proxyPort}`, () => {}).end();\n    await promise;\n  });\n\n  it(\"should proxy requests to multiple servers with different options\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    const source1 = http.createServer(function (req: any, res: any) {\n      expect(req.method).to.eql(\"GET\");\n      expect(Number.parseInt(req.headers.host!.split(\":\")[1])).to.eql(proxyPort);\n      expect(req.url).to.eql(\"/test1\");\n    });\n\n    const source2 = http.createServer(function (req: any, res: any) {\n      source1.close();\n      source2.close();\n      proxyServer.close();\n      expect(req.method).to.eql(\"GET\");\n      expect(Number.parseInt(req.headers.host!.split(\":\")[1])).to.eql(proxyPort);\n      expect(req.url).to.eql(\"/test2\");\n      resolve();\n    });\n\n    const [source1Port, source2Port] = await Promise.all([listenOn(source1), listenOn(source2)]);\n\n    const proxy = httpProxy.createProxyServer();\n\n    // proxies to two servers depending on url, rewriting the url as well\n    function requestHandler(req: any, res: any) {\n      if (req.url.indexOf(\"/s1/\") === 0) {\n        proxy.web(req, res, {\n          ignorePath: true,\n          target: `http://127.0.0.1:${source1Port}` + req.url.slice(3),\n        });\n      } else {\n        proxy.web(req, res, {\n          target: `http://127.0.0.1:${source2Port}`,\n        });\n      }\n    }\n\n    const proxyServer = http.createServer(requestHandler);\n    const proxyPort = await listenOn(proxyServer);\n\n    http.request(`http://127.0.0.1:${proxyPort}/s1/test1`, () => {}).end();\n    http.request(`http://127.0.0.1:${proxyPort}/test2`, () => {}).end();\n    await promise;\n  });\n});\n\ndescribe(\"#client abort propagation\", () => {\n  it(\"should abort proxy request when client disconnects\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    // Target server that waits long enough for client to abort\n    const source = http.createServer((req, _res) => {\n      req.on(\"close\", () => {\n        source.close();\n        proxyServer.close();\n        resolve();\n      });\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n    });\n\n    const proxyServer = http.createServer((req, res) => {\n      proxy.web(req, res);\n    });\n    const proxyPort = await listenOn(proxyServer);\n\n    // Make a request and abort it immediately\n    const req = http.request(`http://127.0.0.1:${proxyPort}`, () => {});\n    req.on(\"error\", () => {}); // Ignore client-side errors from destroy\n    req.end();\n    req.on(\"socket\", () => {\n      // Destroy after connection is established\n      setTimeout(() => req.destroy(), 50);\n    });\n\n    await promise;\n  });\n});\n\n// Regression: upstream http-party/node-http-proxy#1634\n// When req.socket is undefined, the error handler in stream middleware\n// would throw TypeError: Cannot read properties of undefined (reading 'destroyed')\ndescribe(\"#req.socket undefined\", () => {\n  it(\"should not crash when req.socket is undefined and error occurs\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    // Use a port that refuses connections to trigger the error handler\n    const proxy = httpProxy.createProxyServer({\n      target: \"http://127.0.0.1:1\",\n    });\n\n    const proxyServer = http.createServer((req, res) => {\n      // Set req.socket to undefined after proxyReq is emitted but before\n      // the error handler fires (simulates HTTP/2 or edge-case teardown)\n      proxy.once(\"proxyReq\", () => {\n        Object.defineProperty(req, \"socket\", {\n          value: undefined,\n          writable: true,\n          configurable: true,\n        });\n      });\n      proxy.web(req, res);\n    });\n    const proxyPort = await listenOn(proxyServer);\n\n    proxy.once(\"error\", () => {\n      // Should reach here without TypeError crash\n      proxyServer.close();\n      resolve();\n    });\n\n    const req = http.request(`http://127.0.0.1:${proxyPort}`, () => {});\n    req.on(\"error\", () => {}); // Ignore client-side errors\n    req.end();\n\n    await promise;\n  });\n});\n\ndescribe(\"#followRedirects\", () => {\n  it(\"should follow 301 redirect\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    const source = http.createServer(function (req: any, res: any) {\n      if (new URL(req.url, \"http://localhost\").pathname === \"/redirect\") {\n        res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n        res.end(\"ok\");\n        return;\n      }\n      res.writeHead(301, { Location: \"/redirect\" });\n      res.end();\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n      followRedirects: true,\n    });\n\n    const proxyServer = http.createServer((req, res) => proxy.web(req, res));\n    const proxyPort = await listenOn(proxyServer);\n\n    http\n      .request(`http://127.0.0.1:${proxyPort}`, function (res) {\n        source.close();\n        proxyServer.close();\n        expect(res.statusCode).to.eql(200);\n        resolve();\n      })\n      .end();\n    await promise;\n  });\n\n  it(\"should follow 302 and change method to GET\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    const source = http.createServer(function (req: any, res: any) {\n      if (new URL(req.url, \"http://localhost\").pathname === \"/dest\") {\n        res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n        res.end(req.method);\n        return;\n      }\n      res.writeHead(302, { Location: \"/dest\" });\n      res.end();\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n      followRedirects: true,\n    });\n\n    const proxyServer = http.createServer((req, res) => proxy.web(req, res));\n    const proxyPort = await listenOn(proxyServer);\n\n    http\n      .request(`http://127.0.0.1:${proxyPort}`, { method: \"POST\" }, function (res) {\n        let body = \"\";\n        res.on(\"data\", (chunk: Buffer) => (body += chunk));\n        res.on(\"end\", () => {\n          source.close();\n          proxyServer.close();\n          expect(res.statusCode).to.eql(200);\n          expect(body).to.eql(\"GET\");\n          resolve();\n        });\n      })\n      .end(\"post body\");\n    await promise;\n  });\n\n  it(\"should follow 307 preserving method and body\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n    const postBody = \"test-body-content\";\n\n    const source = http.createServer(function (req: any, res: any) {\n      if (new URL(req.url, \"http://localhost\").pathname === \"/dest\") {\n        let body = \"\";\n        req.on(\"data\", (chunk: Buffer) => (body += chunk));\n        req.on(\"end\", () => {\n          res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n          res.end(`${req.method}:${body}`);\n        });\n        return;\n      }\n      res.writeHead(307, { Location: \"/dest\" });\n      res.end();\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n      followRedirects: true,\n    });\n\n    const proxyServer = http.createServer((req, res) => proxy.web(req, res));\n    const proxyPort = await listenOn(proxyServer);\n\n    const proxyReq = http.request(\n      `http://127.0.0.1:${proxyPort}`,\n      { method: \"POST\" },\n      function (res) {\n        let body = \"\";\n        res.on(\"data\", (chunk: Buffer) => (body += chunk));\n        res.on(\"end\", () => {\n          source.close();\n          proxyServer.close();\n          expect(res.statusCode).to.eql(200);\n          expect(body).to.eql(`POST:${postBody}`);\n          resolve();\n        });\n      },\n    );\n    proxyReq.end(postBody);\n    await promise;\n  });\n\n  it(\"should respect numeric max redirects limit\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n    let redirectCount = 0;\n\n    const source = http.createServer(function (_req: any, res: any) {\n      redirectCount++;\n      res.writeHead(301, { Location: `/hop-${redirectCount}` });\n      res.end();\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n      followRedirects: 2,\n    });\n\n    const proxyServer = http.createServer((req, res) => proxy.web(req, res));\n    const proxyPort = await listenOn(proxyServer);\n\n    http\n      .request(`http://127.0.0.1:${proxyPort}`, function (res) {\n        source.close();\n        proxyServer.close();\n        // After 2 redirects followed, the 3rd 301 is returned to client\n        expect(res.statusCode).to.eql(301);\n        expect(redirectCount).to.eql(3);\n        resolve();\n      })\n      .end();\n    await promise;\n  });\n\n  it(\"should handle relative Location headers\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n\n    const source = http.createServer(function (req: any, res: any) {\n      if (new URL(req.url, \"http://localhost\").pathname === \"/sub/dest\") {\n        res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n        res.end(\"ok\");\n        return;\n      }\n      res.writeHead(302, { Location: \"/sub/dest\" });\n      res.end();\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n      followRedirects: true,\n    });\n\n    const proxyServer = http.createServer((req, res) => proxy.web(req, res));\n    const proxyPort = await listenOn(proxyServer);\n\n    http\n      .request(`http://127.0.0.1:${proxyPort}/sub/start`, function (res) {\n        source.close();\n        proxyServer.close();\n        expect(res.statusCode).to.eql(200);\n        resolve();\n      })\n      .end();\n    await promise;\n  });\n\n  it(\"should emit proxyRes only for the final response\", async () => {\n    const { resolve, promise } = Promise.withResolvers<void>();\n    let proxyResCount = 0;\n\n    const source = http.createServer(function (req: any, res: any) {\n      if (new URL(req.url, \"http://localhost\").pathname === \"/final\") {\n        res.writeHead(200, { \"Content-Type\": \"text/plain\" });\n        res.end(\"done\");\n        return;\n      }\n      res.writeHead(302, { Location: \"/final\" });\n      res.end();\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n      followRedirects: true,\n    });\n    proxy.on(\"proxyRes\", () => proxyResCount++);\n\n    const proxyServer = http.createServer((req, res) => proxy.web(req, res));\n    const proxyPort = await listenOn(proxyServer);\n\n    http\n      .request(`http://127.0.0.1:${proxyPort}`, function (res) {\n        let body = \"\";\n        res.on(\"data\", (chunk: Buffer) => (body += chunk));\n        res.on(\"end\", () => {\n          source.close();\n          proxyServer.close();\n          expect(res.statusCode).to.eql(200);\n          expect(body).to.eql(\"done\");\n          expect(proxyResCount).to.eql(1);\n          resolve();\n        });\n      })\n      .end();\n    await promise;\n  });\n});\n\n// Regression: upstream http-party/node-http-proxy#1559\n// req.on('aborted') stopped firing on Node 15.5+ and was later removed,\n// causing a memory leak from accumulated listeners that never get cleaned up.\ndescribe(\"#req-aborted-memory-leak\", () => {\n  it(\"should not attach deprecated 'aborted' listener on req\", async () => {\n    const { promise, resolve } = Promise.withResolvers<void>();\n\n    const source = http.createServer((_req, res) => {\n      res.writeHead(200);\n      res.end(\"ok\");\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxyServer = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n    });\n\n    // Intercept the incoming request to inspect its listeners\n    const server = http.createServer((req, res) => {\n      const abortedBefore = req.listenerCount(\"aborted\");\n      proxyServer.web(req, res).then(() => {\n        const abortedAfter = req.listenerCount(\"aborted\");\n        const addedAbortedListeners = abortedAfter - abortedBefore;\n\n        expect(addedAbortedListeners).to.eql(0);\n\n        source.close();\n        server.close();\n        proxyServer.close();\n        resolve();\n      });\n    });\n    const port = await listenOn(server);\n\n    http.get(`http://127.0.0.1:${port}/test`);\n    await promise;\n  });\n\n  it(\"should abort upstream request when client disconnects via res close\", async () => {\n    const { promise, resolve } = Promise.withResolvers<void>();\n\n    let upstreamAborted = false;\n    const source = http.createServer((req, res) => {\n      res.writeHead(200, { \"content-type\": \"text/plain\" });\n      res.write(\"start\");\n      req.on(\"close\", () => {\n        upstreamAborted = true;\n      });\n    });\n    const sourcePort = await listenOn(source);\n\n    const proxy = httpProxy.createProxyServer({\n      target: `http://127.0.0.1:${sourcePort}`,\n    });\n    const proxyPort = await proxyListen(proxy);\n\n    const clientReq = http.get(`http://127.0.0.1:${proxyPort}/stream`, (res) => {\n      res.once(\"data\", () => {\n        // Client received first chunk; now abort\n        clientReq.destroy();\n\n        setTimeout(() => {\n          expect(upstreamAborted).to.eql(true);\n          source.close();\n          proxy.close(resolve);\n        }, 100);\n      });\n    });\n\n    await promise;\n  });\n});\n"
  },
  {
    "path": "test/middleware/web-outgoing.test.ts",
    "content": "import { describe, it, expect, beforeEach } from \"vitest\";\n\nimport * as webOutgoing from \"../../src/middleware/web-outgoing.ts\";\nimport { stubIncomingMessage, stubServerResponse, stubMiddlewareOptions } from \"../_stubs.ts\";\n\n// Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-http-proxy-passes-web-outgoing-test.js\n\ndescribe(\"middleware:web-outgoing\", () => {\n  const ctx: any = {};\n\n  describe(\"#setRedirectHostRewrite\", () => {\n    beforeEach(() => {\n      ctx.req = {\n        headers: {\n          host: \"ext-auto.com\",\n        },\n      };\n      ctx.proxyRes = {\n        statusCode: 301,\n        headers: {\n          location: \"http://backend.com/\",\n        },\n      };\n      ctx.options = {\n        target: \"http://backend.com\",\n      };\n    });\n\n    describe(\"rewrites location host with hostRewrite\", () => {\n      beforeEach(() => {\n        ctx.options.hostRewrite = \"ext-manual.com\";\n      });\n      for (const code of [201, 301, 302, 303, 307, 308]) {\n        it(\"on \" + code, () => {\n          ctx.proxyRes.statusCode = code;\n          webOutgoing.setRedirectHostRewrite(\n            ctx.req,\n            stubServerResponse(),\n            ctx.proxyRes,\n            ctx.options,\n          );\n          expect(ctx.proxyRes.headers.location).to.eql(\"http://ext-manual.com/\");\n        });\n      }\n\n      it(\"not on 200\", () => {\n        ctx.proxyRes.statusCode = 200;\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://backend.com/\");\n      });\n\n      it(\"not when hostRewrite is unset\", () => {\n        delete ctx.options.hostRewrite;\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://backend.com/\");\n      });\n\n      it(\"takes precedence over autoRewrite\", () => {\n        ctx.options.autoRewrite = true;\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://ext-manual.com/\");\n      });\n\n      it(\"not when the redirected location does not match target host\", () => {\n        ctx.proxyRes.statusCode = 302;\n        ctx.proxyRes.headers.location = \"http://some-other/\";\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://some-other/\");\n      });\n\n      it(\"not when the redirected location does not match target port\", () => {\n        ctx.proxyRes.statusCode = 302;\n        ctx.proxyRes.headers.location = \"http://backend.com:8080/\";\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://backend.com:8080/\");\n      });\n\n      it(\"handles relative Location URLs (issue #20)\", () => {\n        ctx.proxyRes.statusCode = 201;\n        ctx.proxyRes.headers.location = \"/api/books/1\";\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://ext-manual.com/api/books/1\");\n      });\n\n      it(\"handles relative Location URLs with path\", () => {\n        ctx.proxyRes.statusCode = 301;\n        ctx.proxyRes.headers.location = \"/redirect/here?foo=bar\";\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://ext-manual.com/redirect/here?foo=bar\");\n      });\n    });\n\n    describe(\"rewrites location host with autoRewrite\", () => {\n      beforeEach(() => {\n        ctx.options.autoRewrite = true;\n        delete ctx.req.headers[\":authority\"];\n      });\n      for (const code of [201, 301, 302, 303, 307, 308]) {\n        it(\"on \" + code, () => {\n          ctx.proxyRes.statusCode = code;\n          webOutgoing.setRedirectHostRewrite(\n            ctx.req,\n            stubServerResponse(),\n            ctx.proxyRes,\n            ctx.options,\n          );\n          expect(ctx.proxyRes.headers.location).to.eql(\"http://ext-auto.com/\");\n        });\n      }\n\n      it(\"with HTTP/2 :authority\", () => {\n        ctx.req.headers[\":authority\"] = \"ext-auto.com\";\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://ext-auto.com/\");\n      });\n\n      it(\"not on 200\", () => {\n        ctx.proxyRes.statusCode = 200;\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://backend.com/\");\n      });\n\n      it(\"not when autoRewrite is unset\", () => {\n        delete ctx.options.autoRewrite;\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://backend.com/\");\n      });\n\n      it(\"not when the redirected location does not match target host\", () => {\n        ctx.proxyRes.statusCode = 302;\n        ctx.proxyRes.headers.location = \"http://some-other/\";\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://some-other/\");\n      });\n\n      it(\"not when the redirected location does not match target port\", () => {\n        ctx.proxyRes.statusCode = 302;\n        ctx.proxyRes.headers.location = \"http://backend.com:8080/\";\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://backend.com:8080/\");\n      });\n    });\n\n    describe(\"handles object target (ProxyTargetDetailed)\", () => {\n      it(\"rewrites location when target is an object with hostRewrite\", () => {\n        ctx.options.target = {\n          protocol: \"http:\",\n          host: \"backend.com\",\n          hostname: \"backend.com\",\n        };\n        ctx.options.hostRewrite = \"ext-manual.com\";\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://ext-manual.com/\");\n      });\n\n      it(\"rewrites location when target is a URL instance\", () => {\n        ctx.options.target = new URL(\"http://backend.com\");\n        ctx.options.hostRewrite = \"ext-manual.com\";\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://ext-manual.com/\");\n      });\n    });\n\n    describe(\"rewrites location protocol with protocolRewrite\", () => {\n      beforeEach(() => {\n        ctx.options.protocolRewrite = \"https\";\n      });\n      for (const code of [201, 301, 302, 303, 307, 308]) {\n        it(\"on \" + code, () => {\n          ctx.proxyRes.statusCode = code;\n          webOutgoing.setRedirectHostRewrite(\n            ctx.req,\n            stubServerResponse(),\n            ctx.proxyRes,\n            ctx.options,\n          );\n          expect(ctx.proxyRes.headers.location).to.eql(\"https://backend.com/\");\n        });\n      }\n\n      it(\"not on 200\", () => {\n        ctx.proxyRes.statusCode = 200;\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://backend.com/\");\n      });\n\n      it(\"not when protocolRewrite is unset\", () => {\n        delete ctx.options.protocolRewrite;\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"http://backend.com/\");\n      });\n\n      it(\"works together with hostRewrite\", () => {\n        ctx.options.hostRewrite = \"ext-manual.com\";\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"https://ext-manual.com/\");\n      });\n\n      it(\"works together with autoRewrite\", () => {\n        ctx.options.autoRewrite = true;\n        webOutgoing.setRedirectHostRewrite(\n          ctx.req,\n          stubServerResponse(),\n          ctx.proxyRes,\n          ctx.options,\n        );\n        expect(ctx.proxyRes.headers.location).to.eql(\"https://ext-auto.com/\");\n      });\n    });\n  });\n\n  describe(\"#setConnection\", () => {\n    it(\"set the right connection with 1.0 - `close`\", () => {\n      const proxyRes = stubIncomingMessage({ headers: {} });\n      webOutgoing.setConnection(\n        stubIncomingMessage({\n          httpVersion: \"1.0\",\n          headers: { connection: undefined },\n        }),\n        stubServerResponse(),\n        proxyRes,\n        stubMiddlewareOptions(),\n      );\n\n      expect(proxyRes.headers.connection).to.eql(\"close\");\n    });\n\n    it(\"set the right connection with 1.0 - req.connection\", () => {\n      const proxyRes = stubIncomingMessage({ headers: {} });\n      webOutgoing.setConnection(\n        stubIncomingMessage({\n          httpVersion: \"1.0\",\n          headers: { connection: \"hey\" },\n        }),\n        stubServerResponse(),\n        proxyRes,\n        stubMiddlewareOptions(),\n      );\n\n      expect(proxyRes.headers.connection).to.eql(\"hey\");\n    });\n\n    it(\"set the right connection - req.connection\", () => {\n      const proxyRes = stubIncomingMessage({ headers: {} });\n      webOutgoing.setConnection(\n        stubIncomingMessage({\n          httpVersion: undefined,\n          headers: { connection: \"hola\" },\n        }),\n        stubServerResponse(),\n        proxyRes,\n        stubMiddlewareOptions(),\n      );\n\n      expect(proxyRes.headers.connection).to.eql(\"hola\");\n    });\n\n    it(\"set the right connection (HTTP/1.1) - req.connection\", () => {\n      const proxyRes = { headers: {} as any };\n      webOutgoing.setConnection(\n        {\n          httpVersion: \"1.0\",\n          httpVersionMajor: 1,\n          headers: {\n            connection: \"hola\",\n          },\n        } as any,\n        {} as any,\n        proxyRes as any,\n        {} as any,\n      );\n\n      expect(proxyRes.headers.connection).to.eql(\"hola\");\n    });\n\n    it(\"set the right connection (HTTP/2) - req.connection\", () => {\n      const proxyRes = { headers: {} as any };\n      webOutgoing.setConnection(\n        {\n          httpVersion: \"2.0\",\n          httpVersionMajor: 2,\n          headers: {\n            connection: \"hola\",\n          },\n        } as any,\n        {} as any,\n        proxyRes as any,\n        {} as any,\n      );\n\n      expect(proxyRes.headers.connection).to.eql(undefined);\n    });\n\n    it(\"set the right connection - `keep-alive`\", () => {\n      const proxyRes = stubIncomingMessage({ headers: {} });\n      webOutgoing.setConnection(\n        stubIncomingMessage({\n          httpVersion: undefined,\n          headers: { connection: undefined },\n        }),\n        stubServerResponse(),\n        proxyRes,\n        stubMiddlewareOptions(),\n      );\n\n      expect(proxyRes.headers.connection).to.eql(\"keep-alive\");\n    });\n\n    it(\"don`t set connection with 2.0 if exist\", () => {\n      const proxyRes = stubIncomingMessage({ headers: {} });\n      webOutgoing.setConnection(\n        stubIncomingMessage({\n          httpVersion: \"2.0\",\n          httpVersionMajor: 2,\n          headers: { connection: \"namstey\" },\n        }),\n        stubServerResponse(),\n        proxyRes,\n        stubMiddlewareOptions(),\n      );\n\n      expect(proxyRes.headers.connection).to.eql(undefined);\n    });\n\n    it(\"don`t set connection with 2.0 if doesn`t exist\", () => {\n      const proxyRes = stubIncomingMessage({ headers: {} });\n      webOutgoing.setConnection(\n        stubIncomingMessage({\n          httpVersion: \"2.0\",\n          httpVersionMajor: 2,\n          headers: {},\n        }),\n        stubServerResponse(),\n        proxyRes,\n        stubMiddlewareOptions(),\n      );\n\n      expect(proxyRes.headers.connection as any).to.eql(undefined);\n    });\n  });\n\n  describe(\"#writeStatusCode\", () => {\n    it(\"should write status code\", () => {\n      const res = stubServerResponse({\n        writeHead: function (n: number) {\n          expect(n).to.eql(200);\n        },\n      });\n\n      webOutgoing.writeStatusCode(\n        stubIncomingMessage(),\n        res,\n        stubIncomingMessage({ statusCode: 200 }),\n        stubMiddlewareOptions(),\n      );\n    });\n\n    it(\"should write status code with statusMessage\", () => {\n      const res = stubServerResponse();\n      webOutgoing.writeStatusCode(\n        stubIncomingMessage(),\n        res,\n        stubIncomingMessage({ statusCode: 404, statusMessage: \"Not Found\" }),\n        stubMiddlewareOptions(),\n      );\n      expect(res.statusCode).to.eql(404);\n      expect(res.statusMessage).to.eql(\"Not Found\");\n    });\n\n    it(\"should write status code without statusMessage\", () => {\n      const res = stubServerResponse();\n      webOutgoing.writeStatusCode(\n        stubIncomingMessage(),\n        res,\n        stubIncomingMessage({ statusCode: 200 }),\n        stubMiddlewareOptions(),\n      );\n      expect(res.statusCode).to.eql(200);\n      expect(res.statusMessage).to.eql(undefined);\n    });\n  });\n\n  describe(\"#writeHeaders\", () => {\n    beforeEach(() => {\n      ctx.proxyRes = {\n        headers: {\n          hey: \"hello\",\n          how: \"are you?\",\n          \"set-cookie\": [\"hello; domain=my.domain; path=/\", \"there; domain=my.domain; path=/\"],\n        },\n      };\n      ctx.rawProxyRes = {\n        headers: {\n          hey: \"hello\",\n          how: \"are you?\",\n          \"set-cookie\": [\"hello; domain=my.domain; path=/\", \"there; domain=my.domain; path=/\"],\n        },\n        rawHeaders: [\n          \"Hey\",\n          \"hello\",\n          \"How\",\n          \"are you?\",\n          \"Set-Cookie\",\n          \"hello; domain=my.domain; path=/\",\n          \"Set-Cookie\",\n          \"there; domain=my.domain; path=/\",\n        ],\n      };\n      ctx.res = {\n        setHeader: function (k: string, v: string) {\n          // https://nodejs.org/api/http.html#http_message_headers\n          // Header names are lower-cased\n          ctx.res.headers[k.toLowerCase()] = v;\n        },\n        headers: {} as Record<string, any>,\n      };\n    });\n\n    it(\"writes headers\", () => {\n      const options = {};\n      webOutgoing.writeHeaders(stubIncomingMessage(), ctx.res, ctx.proxyRes, options as any);\n\n      expect(ctx.res.headers.hey).to.eql(\"hello\");\n      expect(ctx.res.headers.how).to.eql(\"are you?\");\n\n      expect(ctx.res.headers[\"set-cookie\"]).toBeInstanceOf(Array);\n      expect(ctx.res.headers[\"set-cookie\"]).to.have.length(2);\n    });\n\n    it(\"writes raw headers\", () => {\n      const options = {};\n      webOutgoing.writeHeaders(stubIncomingMessage(), ctx.res, ctx.rawProxyRes, options as any);\n\n      expect(ctx.res.headers.hey).to.eql(\"hello\");\n      expect(ctx.res.headers.how).to.eql(\"are you?\");\n\n      expect(ctx.res.headers[\"set-cookie\"]).toBeInstanceOf(Array);\n      expect(ctx.res.headers[\"set-cookie\"]).to.have.length(2);\n    });\n\n    it(\"rewrites path\", () => {\n      const options = {\n        cookiePathRewrite: \"/dummyPath\",\n      };\n\n      webOutgoing.writeHeaders(stubIncomingMessage(), ctx.res, ctx.proxyRes, options as any);\n\n      expect(ctx.res.headers[\"set-cookie\"]).to.contain(\"hello; domain=my.domain; path=/dummyPath\");\n    });\n\n    it(\"does not rewrite path\", () => {\n      const options = {};\n\n      webOutgoing.writeHeaders(stubIncomingMessage(), ctx.res, ctx.proxyRes, options as any);\n\n      expect(ctx.res.headers[\"set-cookie\"]).to.contain(\"hello; domain=my.domain; path=/\");\n    });\n\n    it(\"removes path\", () => {\n      const options = {\n        cookiePathRewrite: \"\",\n      };\n\n      webOutgoing.writeHeaders(stubIncomingMessage(), ctx.res, ctx.proxyRes, options as any);\n\n      expect(ctx.res.headers[\"set-cookie\"]).to.contain(\"hello; domain=my.domain\");\n    });\n\n    it(\"does not rewrite domain\", () => {\n      const options = {};\n\n      webOutgoing.writeHeaders(stubIncomingMessage(), ctx.res, ctx.proxyRes, options as any);\n\n      expect(ctx.res.headers[\"set-cookie\"]).to.contain(\"hello; domain=my.domain; path=/\");\n    });\n\n    it(\"rewrites domain\", () => {\n      const options = {\n        cookieDomainRewrite: \"my.new.domain\",\n      };\n\n      webOutgoing.writeHeaders(stubIncomingMessage(), ctx.res, ctx.proxyRes, options as any);\n\n      expect(ctx.res.headers[\"set-cookie\"]).to.contain(\"hello; domain=my.new.domain; path=/\");\n    });\n\n    it(\"removes domain\", () => {\n      const options = {\n        cookieDomainRewrite: \"\",\n      };\n\n      webOutgoing.writeHeaders(stubIncomingMessage(), ctx.res, ctx.proxyRes, options as any);\n\n      expect(ctx.res.headers[\"set-cookie\"]).to.contain(\"hello; path=/\");\n    });\n\n    it(\"rewrites headers with advanced configuration\", () => {\n      const options = {\n        cookieDomainRewrite: {\n          \"*\": \"\",\n          \"my.old.domain\": \"my.new.domain\",\n          \"my.special.domain\": \"my.special.domain\",\n        },\n      };\n      ctx.proxyRes.headers[\"set-cookie\"] = [\n        \"hello-on-my.domain; domain=my.domain; path=/\",\n        \"hello-on-my.old.domain; domain=my.old.domain; path=/\",\n        \"hello-on-my.special.domain; domain=my.special.domain; path=/\",\n      ];\n      webOutgoing.writeHeaders(stubIncomingMessage(), ctx.res, ctx.proxyRes, options as any);\n\n      expect(ctx.res.headers[\"set-cookie\"]).to.contain(\"hello-on-my.domain; path=/\");\n      expect(ctx.res.headers[\"set-cookie\"]).to.contain(\n        \"hello-on-my.old.domain; domain=my.new.domain; path=/\",\n      );\n      expect(ctx.res.headers[\"set-cookie\"]).to.contain(\n        \"hello-on-my.special.domain; domain=my.special.domain; path=/\",\n      );\n    });\n\n    it(\"rewrites raw headers with advanced configuration\", () => {\n      const options = {\n        cookieDomainRewrite: {\n          \"*\": \"\",\n          \"my.old.domain\": \"my.new.domain\",\n          \"my.special.domain\": \"my.special.domain\",\n        },\n      };\n      ctx.rawProxyRes.headers[\"set-cookie\"] = [\n        \"hello-on-my.domain; domain=my.domain; path=/\",\n        \"hello-on-my.old.domain; domain=my.old.domain; path=/\",\n        \"hello-on-my.special.domain; domain=my.special.domain; path=/\",\n      ];\n      ctx.rawProxyRes.rawHeaders = [\n        ...ctx.rawProxyRes.rawHeaders,\n        \"Set-Cookie\",\n        \"hello-on-my.domain; domain=my.domain; path=/\",\n        \"Set-Cookie\",\n        \"hello-on-my.old.domain; domain=my.old.domain; path=/\",\n        \"Set-Cookie\",\n        \"hello-on-my.special.domain; domain=my.special.domain; path=/\",\n      ];\n      webOutgoing.writeHeaders(stubIncomingMessage(), ctx.res, ctx.rawProxyRes, options as any);\n\n      expect(ctx.res.headers[\"set-cookie\"]).to.include(\"hello-on-my.domain; path=/\");\n      expect(ctx.res.headers[\"set-cookie\"]).to.contain(\n        \"hello-on-my.old.domain; domain=my.new.domain; path=/\",\n      );\n      expect(ctx.res.headers[\"set-cookie\"]).to.contain(\n        \"hello-on-my.special.domain; domain=my.special.domain; path=/\",\n      );\n    });\n\n    it(\"skips invalid headers instead of crashing\", () => {\n      const options = {};\n      const proxyRes = {\n        headers: {\n          hey: \"hello\",\n          how: \"are you?\",\n          \"invalid-header\": \"value\\u0001with\\u0002invalid\\u0003chars\",\n          \"set-cookie\": [\"hello; domain=my.domain; path=/\", \"there; domain=my.domain; path=/\"],\n        },\n      };\n      // Use a real-ish setHeader that validates like Node.js does\n      const headers: Record<string, any> = {};\n      const res = {\n        setHeader(k: string, v: string | string[]) {\n          // Simulate Node.js ERR_INVALID_CHAR for control characters\n          // eslint-disable-next-line no-control-regex)\n          if (typeof v === \"string\" && /[\\u0000-\\u001F]/.test(v)) {\n            throw new TypeError(`Invalid character in header content [\"${k}\"]`);\n          }\n          headers[k.toLowerCase()] = v;\n        },\n      };\n      webOutgoing.writeHeaders(stubIncomingMessage(), res as any, proxyRes as any, options as any);\n\n      // Valid headers should still be set\n      expect(headers.hey).to.eql(\"hello\");\n      expect(headers.how).to.eql(\"are you?\");\n      expect(headers[\"set-cookie\"]).toBeInstanceOf(Array);\n      expect(headers[\"set-cookie\"]).to.have.length(2);\n      // Invalid header should be skipped (not set)\n      expect(headers).to.not.have.key(\"invalid-header\");\n    });\n\n    it(\"skips empty header names (upstream#1551)\", () => {\n      const proxyRes = {\n        headers: {\n          \"\": \"empty-key-value\",\n          \"  \": \"whitespace-key-value\",\n          hey: \"hello\",\n        },\n      };\n      const headers: Record<string, any> = {};\n      const res = {\n        setHeader(k: string, v: string | string[]) {\n          // Simulate Node.js ERR_INVALID_HTTP_TOKEN for empty/invalid header names\n          if (!k || !/^[\\w!#$%&'*+\\-.^`|~]+$/.test(k)) {\n            throw new TypeError(`Header name must be a valid HTTP token [\"${k}\"]`);\n          }\n          headers[k.toLowerCase()] = v;\n        },\n      };\n      webOutgoing.writeHeaders(\n        stubIncomingMessage(),\n        res as any,\n        proxyRes as any,\n        stubMiddlewareOptions(),\n      );\n      // Valid headers should still be set\n      expect(headers.hey).to.eql(\"hello\");\n      // Empty/whitespace-only header names should be skipped\n      expect(headers).to.not.have.key(\"\");\n      expect(headers).to.not.have.key(\"  \");\n    });\n\n    it(\"skips undefined header values\", () => {\n      const proxyRes = {\n        headers: {\n          hey: \"hello\",\n          undef: undefined,\n        },\n      };\n      const headers: any = {};\n      const res = {\n        setHeader: function (k: string, v: string) {\n          headers[k.toLowerCase()] = v;\n        },\n      };\n      webOutgoing.writeHeaders(\n        stubIncomingMessage(),\n        res as any,\n        proxyRes as any,\n        stubMiddlewareOptions(),\n      );\n      expect(headers.hey).to.eql(\"hello\");\n      expect(headers).to.not.have.key(\"undef\");\n    });\n  });\n\n  describe(\"#removeChunked\", () => {\n    it(\"removes transfer-encoding on HTTP/1.0\", () => {\n      const proxyRes = {\n        headers: {\n          \"transfer-encoding\": \"hello\",\n        },\n      };\n      webOutgoing.removeChunked(\n        stubIncomingMessage({ httpVersion: \"1.0\" }),\n        stubServerResponse(),\n        proxyRes as any,\n        stubMiddlewareOptions(),\n      );\n      expect(proxyRes.headers[\"transfer-encoding\"]).to.eql(undefined);\n    });\n\n    it(\"removes transfer-encoding on 204 response\", () => {\n      const proxyRes = {\n        statusCode: 204,\n        headers: {\n          \"transfer-encoding\": \"chunked\",\n        },\n      };\n      webOutgoing.removeChunked(\n        stubIncomingMessage({ httpVersion: \"1.1\" }),\n        stubServerResponse(),\n        proxyRes as any,\n        stubMiddlewareOptions(),\n      );\n      expect(proxyRes.headers[\"transfer-encoding\"]).to.eql(undefined);\n    });\n\n    it(\"removes transfer-encoding on 304 response\", () => {\n      const proxyRes = {\n        statusCode: 304,\n        headers: {\n          \"transfer-encoding\": \"chunked\",\n        },\n      };\n      webOutgoing.removeChunked(\n        stubIncomingMessage({ httpVersion: \"1.1\" }),\n        stubServerResponse(),\n        proxyRes as any,\n        stubMiddlewareOptions(),\n      );\n      expect(proxyRes.headers[\"transfer-encoding\"]).to.eql(undefined);\n    });\n\n    it(\"preserves transfer-encoding on normal HTTP/1.1 responses\", () => {\n      const proxyRes = {\n        statusCode: 200,\n        headers: {\n          \"transfer-encoding\": \"chunked\",\n        },\n      };\n      webOutgoing.removeChunked(\n        stubIncomingMessage({ httpVersion: \"1.1\" }),\n        stubServerResponse(),\n        proxyRes as any,\n        stubMiddlewareOptions(),\n      );\n      expect(proxyRes.headers[\"transfer-encoding\"]).to.eql(\"chunked\");\n    });\n  });\n});\n"
  },
  {
    "path": "test/middleware/ws-incoming.test.ts",
    "content": "import { describe, it, expect } from \"vitest\";\nimport * as wsIncoming from \"../../src/middleware/ws-incoming.ts\";\nimport {\n  stubIncomingMessage,\n  stubSocket,\n  stubMiddlewareOptions,\n  stubProxyServer,\n} from \"../_stubs.ts\";\n\n// Source: https://github.com/http-party/node-http-proxy/blob/master/test/lib-http-proxy-passes-ws-incoming-test.js\n\ndescribe(\"middleware:ws-incoming\", () => {\n  describe(\"#checkMethodAndHeader\", () => {\n    it(\"should drop non-GET connections\", () => {\n      let destroyCalled = false;\n      const returnValue = wsIncoming.checkMethodAndHeader(\n        stubIncomingMessage({ method: \"DELETE\", headers: {} }),\n        stubSocket({\n          destroy: () => {\n            destroyCalled = true;\n          },\n        }),\n        stubMiddlewareOptions(),\n        stubProxyServer(),\n      );\n      expect(returnValue).toBe(true);\n      expect(destroyCalled).toBe(true);\n    });\n\n    it(\"should drop connections when no upgrade header\", () => {\n      let destroyCalled = false;\n      const returnValue = wsIncoming.checkMethodAndHeader(\n        stubIncomingMessage({ method: \"GET\", headers: {} }),\n        stubSocket({\n          destroy: () => {\n            destroyCalled = true;\n          },\n        }),\n        stubMiddlewareOptions(),\n        stubProxyServer(),\n      );\n      expect(returnValue).toBe(true);\n      expect(destroyCalled).toBe(true);\n    });\n\n    it(\"should drop connections when upgrade header is different of `websocket`\", () => {\n      let destroyCalled = false;\n      const returnValue = wsIncoming.checkMethodAndHeader(\n        stubIncomingMessage({\n          method: \"GET\",\n          headers: { upgrade: \"anotherprotocol\" },\n        }),\n        stubSocket({\n          destroy: () => {\n            destroyCalled = true;\n          },\n        }),\n        stubMiddlewareOptions(),\n        stubProxyServer(),\n      );\n      expect(returnValue).toBe(true);\n      expect(destroyCalled).toBe(true);\n    });\n\n    it(\"should return nothing when all is ok\", () => {\n      let destroyCalled = false;\n      const returnValue = wsIncoming.checkMethodAndHeader(\n        stubIncomingMessage({\n          method: \"GET\",\n          headers: { upgrade: \"websocket\" },\n        }),\n        stubSocket({\n          destroy: () => {\n            destroyCalled = true;\n          },\n        }),\n        stubMiddlewareOptions(),\n        stubProxyServer(),\n      );\n      expect(returnValue).toBe(undefined);\n      expect(destroyCalled).toBe(false);\n    });\n  });\n\n  describe(\"#XHeaders\", () => {\n    it(\"return if no forward request\", () => {\n      const returnValue = wsIncoming.XHeaders(\n        stubIncomingMessage(),\n        stubSocket(),\n        stubMiddlewareOptions(),\n        stubProxyServer(),\n      );\n      expect(returnValue).toBe(undefined);\n    });\n\n    it(\"set the correct x-forwarded-* headers from req.connection\", () => {\n      const req = stubIncomingMessage({\n        connection: {\n          remoteAddress: \"192.168.1.2\",\n          remotePort: \"8080\",\n        },\n        headers: {\n          host: \"192.168.1.2:8080\",\n        },\n      });\n      wsIncoming.XHeaders(\n        req,\n        stubSocket(),\n        stubMiddlewareOptions({ xfwd: true }),\n        stubProxyServer(),\n      );\n      expect(req.headers[\"x-forwarded-for\"]).toBe(\"192.168.1.2\");\n      expect(req.headers[\"x-forwarded-port\"]).toBe(\"8080\");\n      expect(req.headers[\"x-forwarded-proto\"]).toBe(\"ws\");\n    });\n\n    it(\"set the correct x-forwarded-* headers from req.socket\", () => {\n      const req = stubIncomingMessage({\n        socket: {\n          remoteAddress: \"192.168.1.3\",\n          remotePort: \"8181\",\n          encrypted: true,\n        },\n        connection: {},\n        headers: {\n          host: \"192.168.1.3:8181\",\n        },\n      });\n      wsIncoming.XHeaders(\n        req,\n        stubSocket(),\n        stubMiddlewareOptions({ xfwd: true }),\n        stubProxyServer(),\n      );\n      expect(req.headers[\"x-forwarded-for\"]).toBe(\"192.168.1.3\");\n      expect(req.headers[\"x-forwarded-port\"]).toBe(\"8181\");\n      expect(req.headers[\"x-forwarded-proto\"]).toBe(\"wss\");\n    });\n\n    it(\"should not overwrite existing x-forwarded-* headers\", () => {\n      const req = stubIncomingMessage({\n        socket: {\n          remoteAddress: \"192.168.1.3\",\n          remotePort: \"8181\",\n        },\n        connection: {\n          pair: true,\n        },\n        headers: {\n          host: \"192.168.1.3:8181\",\n          \"x-forwarded-for\": \"192.168.1.2\",\n          \"x-forwarded-port\": \"8182\",\n          \"x-forwarded-proto\": \"ws\",\n        },\n      });\n      wsIncoming.XHeaders(\n        req,\n        stubSocket(),\n        stubMiddlewareOptions({ xfwd: true }),\n        stubProxyServer(),\n      );\n      expect(req.headers[\"x-forwarded-for\"]).toBe(\"192.168.1.2\");\n      expect(req.headers[\"x-forwarded-port\"]).toBe(\"8182\");\n      expect(req.headers[\"x-forwarded-proto\"]).toBe(\"ws\");\n    });\n  });\n});\n"
  },
  {
    "path": "test/server.test.ts",
    "content": "import { describe, it, expect, afterEach, vi } from \"vitest\";\nimport http from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport { createProxyServer, ProxyServer } from \"../src/index.ts\";\n\ndescribe(\"ProxyServer\", () => {\n  let proxy: ProxyServer;\n  let source: http.Server;\n\n  afterEach(() => {\n    if (proxy && (proxy as any)._server) {\n      proxy.close();\n    }\n    source?.close();\n  });\n\n  describe(\"#listen\", () => {\n    it(\"should create an HTTP server and listen\", async () => {\n      source = http.createServer((req, res) => {\n        res.end(\"ok\");\n      });\n      await new Promise<void>((r) => source.listen(0, \"127.0.0.1\", r));\n      const sourcePort = (source.address() as AddressInfo).port;\n\n      proxy = createProxyServer({ target: `http://127.0.0.1:${sourcePort}` });\n\n      const cb = vi.fn();\n\n      proxy.listen(0, \"127.0.0.1\", cb);\n\n      // Wait for the server to be ready\n      await new Promise<void>((r) => setTimeout(r, 50));\n\n      expect(cb).toHaveBeenCalledOnce();\n      expect(cb).toHaveBeenCalledWith();\n\n      const proxyPort = ((proxy as any)._server.address() as AddressInfo).port;\n\n      const res = await fetch(`http://127.0.0.1:${proxyPort}/`);\n      expect(res.status).toBe(200);\n      expect(await res.text()).toBe(\"ok\");\n    });\n\n    it(\"should create an HTTPS server when ssl option is set\", async () => {\n      const { readFileSync } = await import(\"node:fs\");\n      const { join } = await import(\"node:path\");\n\n      // Check if test certs exist\n      let key: Buffer, cert: Buffer;\n      try {\n        key = readFileSync(join(import.meta.dirname, \"fixtures/agent2-key.pem\"));\n        cert = readFileSync(join(import.meta.dirname, \"fixtures/agent2-cert.pem\"));\n      } catch {\n        // Skip if no test certs\n        return;\n      }\n\n      source = http.createServer((req, res) => {\n        res.end(\"ssl-ok\");\n      });\n      await new Promise<void>((r) => source.listen(0, \"127.0.0.1\", r));\n      const sourcePort = (source.address() as AddressInfo).port;\n\n      proxy = createProxyServer({\n        target: `http://127.0.0.1:${sourcePort}`,\n        ssl: { key, cert },\n      });\n      proxy.listen(0, \"127.0.0.1\");\n\n      await new Promise<void>((r) => setTimeout(r, 50));\n      const server = (proxy as any)._server;\n      expect(server).toBeDefined();\n      // It should be an HTTPS server (has cert context)\n      expect(server.constructor.name).toBe(\"Server\");\n    });\n\n    it(\"should set up WebSocket upgrade listener when ws option is set\", async () => {\n      source = http.createServer((req, res) => {\n        res.end(\"ws-ok\");\n      });\n      await new Promise<void>((r) => source.listen(0, \"127.0.0.1\", r));\n      const sourcePort = (source.address() as AddressInfo).port;\n\n      proxy = createProxyServer({\n        target: `http://127.0.0.1:${sourcePort}`,\n        ws: true,\n      });\n      proxy.listen(0, \"127.0.0.1\");\n\n      await new Promise<void>((r) => setTimeout(r, 50));\n      const server = (proxy as any)._server;\n      expect(server.listeners(\"upgrade\").length).toBeGreaterThan(0);\n    });\n  });\n\n  describe(\"#close\", () => {\n    it(\"should close the server and call the callback\", async () => {\n      source = http.createServer((req, res) => {\n        res.end(\"ok\");\n      });\n      await new Promise<void>((r) => source.listen(0, \"127.0.0.1\", r));\n      const sourcePort = (source.address() as AddressInfo).port;\n\n      proxy = createProxyServer({ target: `http://127.0.0.1:${sourcePort}` });\n      proxy.listen(0, \"127.0.0.1\");\n\n      await new Promise<void>((r) => setTimeout(r, 50));\n\n      const closed = await new Promise<boolean>((resolve) => {\n        proxy.close(() => resolve(true));\n      });\n      expect(closed).toBe(true);\n      expect((proxy as any)._server).toBeUndefined();\n    });\n\n    it(\"should not throw when no server exists\", () => {\n      proxy = createProxyServer({});\n      // close without listen should be a no-op\n      proxy.close();\n      expect((proxy as any)._server).toBeUndefined();\n    });\n\n    it(\"should close without callback\", async () => {\n      source = http.createServer((req, res) => {\n        res.end(\"ok\");\n      });\n      await new Promise<void>((r) => source.listen(0, \"127.0.0.1\", r));\n      const sourcePort = (source.address() as AddressInfo).port;\n\n      proxy = createProxyServer({ target: `http://127.0.0.1:${sourcePort}` });\n      proxy.listen(0, \"127.0.0.1\");\n      await new Promise<void>((r) => setTimeout(r, 50));\n\n      // Should not throw\n      proxy.close();\n      await new Promise<void>((r) => setTimeout(r, 50));\n    });\n  });\n\n  describe(\"#before / #after\", () => {\n    it(\"should insert a middleware before a named pass (using empty name for arrow fns)\", () => {\n      proxy = createProxyServer({});\n      const initialLength = proxy._webPasses.length;\n      // Arrow function passes have empty string names; before() finds the last match\n      const customPass = function customMiddleware() {};\n      proxy.before(\"web\", \"\", customPass as any);\n\n      expect(proxy._webPasses.length).toBe(initialLength + 1);\n      // Inserted before the last pass (last match of \"\")\n      expect(proxy._webPasses[initialLength - 1]).toBe(customPass);\n    });\n\n    it(\"should insert a middleware after a named pass\", () => {\n      proxy = createProxyServer({});\n      const initialLength = proxy._webPasses.length;\n      const customPass = function customMiddleware() {};\n      proxy.after(\"web\", \"\", customPass as any);\n\n      expect(proxy._webPasses.length).toBe(initialLength + 1);\n    });\n\n    it(\"should throw for invalid type in before\", () => {\n      proxy = createProxyServer({});\n      expect(() => {\n        proxy.before(\"invalid\" as any, \"\", (() => {}) as any);\n      }).toThrow(\"type must be `web` or `ws`\");\n    });\n\n    it(\"should throw for invalid type in after\", () => {\n      proxy = createProxyServer({});\n      expect(() => {\n        proxy.after(\"invalid\" as any, \"\", (() => {}) as any);\n      }).toThrow(\"type must be `web` or `ws`\");\n    });\n\n    it(\"should throw for non-existent pass name in before\", () => {\n      proxy = createProxyServer({});\n      expect(() => {\n        proxy.before(\"web\", \"nonexistent\", (() => {}) as any);\n      }).toThrow(\"No such pass\");\n    });\n\n    it(\"should throw for non-existent pass name in after\", () => {\n      proxy = createProxyServer({});\n      expect(() => {\n        proxy.after(\"web\", \"nonexistent\", (() => {}) as any);\n      }).toThrow(\"No such pass\");\n    });\n\n    it(\"should work with ws type\", () => {\n      proxy = createProxyServer({});\n      const initialLength = proxy._wsPasses.length;\n      const customPass = function wsMiddleware() {};\n      proxy.before(\"ws\", \"\", customPass as any);\n\n      expect(proxy._wsPasses.length).toBe(initialLength + 1);\n      // Inserted before the last pass (last match of \"\")\n      expect(proxy._wsPasses[initialLength - 1]).toBe(customPass);\n    });\n  });\n\n  describe(\"_createProxyFn error paths\", () => {\n    it(\"should emit error when no target and no forward\", async () => {\n      proxy = createProxyServer({});\n      const { resolve, promise } = Promise.withResolvers<Error>();\n\n      proxy.on(\"error\", (err) => {\n        resolve(err);\n      });\n\n      const stubReq = { url: \"/\", headers: {} } as any;\n      const stubRes = {\n        on: () => {},\n        end: () => {},\n      } as any;\n\n      proxy.web(stubReq, stubRes);\n      const err = await promise;\n      expect(err.message).toBe(\"Must provide a proper URL as target\");\n    });\n\n    it(\"should convert string target to URL\", async () => {\n      source = http.createServer((req, res) => {\n        res.end(\"converted\");\n      });\n      await new Promise<void>((r) => source.listen(0, \"127.0.0.1\", r));\n      const sourcePort = (source.address() as AddressInfo).port;\n\n      proxy = createProxyServer({});\n      const proxyServer = http.createServer((req, res) => {\n        proxy.web(req, res, { target: `http://127.0.0.1:${sourcePort}` });\n      });\n      await new Promise<void>((r) => proxyServer.listen(0, \"127.0.0.1\", r));\n      const proxyPort = (proxyServer.address() as AddressInfo).port;\n\n      const res = await fetch(`http://127.0.0.1:${proxyPort}/`);\n      expect(await res.text()).toBe(\"converted\");\n      proxyServer.close();\n    });\n\n    it(\"should resolve when a pass returns truthy (halt loop)\", async () => {\n      proxy = createProxyServer({ target: \"http://127.0.0.1:1\" });\n      const net = await import(\"node:net\");\n\n      // WS checkMethodAndHeader returns true for non-GET → halts loop\n      const stubReq = { method: \"POST\", headers: {}, url: \"/\" } as any;\n      const socket = new net.Socket();\n\n      await proxy.ws(stubReq, socket, { target: \"http://127.0.0.1:1\" });\n      // Should resolve without error (the socket was destroyed by checkMethodAndHeader)\n      socket.destroy();\n    });\n\n    it(\"should convert string forward to URL\", async () => {\n      source = http.createServer((req, res) => {\n        res.end(\"forward-ok\");\n      });\n      await new Promise<void>((r) => source.listen(0, \"127.0.0.1\", r));\n      const sourcePort = (source.address() as AddressInfo).port;\n\n      proxy = createProxyServer({});\n      const proxyServer = http.createServer((req, res) => {\n        proxy.web(req, res, { forward: `http://127.0.0.1:${sourcePort}` });\n      });\n      await new Promise<void>((r) => proxyServer.listen(0, \"127.0.0.1\", r));\n      const proxyPort = (proxyServer.address() as AddressInfo).port;\n\n      const res = await fetch(`http://127.0.0.1:${proxyPort}/`);\n      // Forward-only ends the response\n      expect(res.status).toBe(200);\n      proxyServer.close();\n    });\n  });\n});\n"
  },
  {
    "path": "test/types.test-d.ts",
    "content": "import { assertType, describe, expectTypeOf, it } from \"vitest\";\nimport { ProxyServer } from \"../src/server.ts\";\nimport type { ProxyUpgradeOptions } from \"../src/ws.ts\";\nimport type { Request as ExpressRequest, Response as ExpressResponse } from \"express\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\n\ndescribe(\"httpxy types\", () => {\n  it(\"ProxyServer generic types\", () => {\n    assertType<ProxyServer>(new ProxyServer());\n    assertType<ProxyServer<IncomingMessage, ServerResponse>>(new ProxyServer());\n    assertType<ProxyServer<ExpressRequest, ExpressResponse>>(\n      new ProxyServer<ExpressRequest, ExpressResponse>(),\n    );\n\n    const expressProxyServer = new ProxyServer<ExpressRequest, ExpressResponse>();\n\n    expressProxyServer.on(\"start\", (req, res) => {\n      expectTypeOf(req).toEqualTypeOf<ExpressRequest>();\n      expectTypeOf(req).toExtend<IncomingMessage>();\n\n      expectTypeOf(res).toEqualTypeOf<ExpressResponse>();\n      expectTypeOf(res).toExtend<ServerResponse>();\n    });\n  });\n\n  it(\"ProxyUpgradeOptions type\", () => {\n    assertType<ProxyUpgradeOptions>({\n      xfwd: true,\n      changeOrigin: true,\n      headers: { \"x-test\": \"1\" },\n      secure: true,\n      localAddress: \"127.0.0.1\",\n      auth: \"user:pass\",\n      prependPath: false,\n      ignorePath: true,\n      toProxy: true,\n    });\n  });\n});\n"
  },
  {
    "path": "test/ws-destroyed-socket.test.ts",
    "content": "import { describe, it } from \"vitest\";\nimport http from \"node:http\";\nimport net from \"node:net\";\nimport type { AddressInfo } from \"node:net\";\nimport { createProxyServer } from \"../src/index.ts\";\nimport { proxyUpgrade } from \"../src/ws.ts\";\n\n/**\n * Regression test for writing to a destroyed socket during WS upgrade.\n * Upstream: https://github.com/http-party/node-http-proxy/pull/1433\n *\n * When a WS upgrade request hits a target that responds with a plain HTTP\n * response (no upgrade), the proxy writes the response back to the client\n * socket. If the client socket is already destroyed by that point, calling\n * `socket.write()` throws and crashes the process.\n */\n\nfunction listenOn(server: http.Server | net.Server): Promise<number> {\n  return new Promise((resolve, reject) => {\n    server.once(\"error\", reject);\n    server.listen(0, \"127.0.0.1\", () => {\n      resolve((server.address() as AddressInfo).port);\n    });\n  });\n}\n\ndescribe(\"ws: write to destroyed socket\", () => {\n  it(\"ProxyServer.ws() should not crash when socket is destroyed before upstream responds\", async () => {\n    // Target server that responds with a normal HTTP 404 (no upgrade)\n    const target = http.createServer((_req, res) => {\n      // Delay so the client socket can be destroyed first\n      setTimeout(() => {\n        res.writeHead(404);\n        res.end(\"Not Found\");\n      }, 50);\n    });\n    const targetPort = await listenOn(target);\n\n    const proxy = createProxyServer({\n      target: `http://127.0.0.1:${targetPort}`,\n      ws: true,\n    });\n\n    // Suppress proxy error events (expected when socket is destroyed)\n    proxy.on(\"error\", () => {});\n\n    const proxyServer = http.createServer();\n    proxyServer.on(\"upgrade\", (req, socket, head) => {\n      // Destroy the socket before the target has a chance to respond\n      setTimeout(() => {\n        socket.destroy();\n      }, 10);\n      proxy.ws(req, socket as net.Socket, {}, head);\n    });\n    const proxyPort = await listenOn(proxyServer);\n\n    // Open a raw TCP connection and send a WS upgrade request\n    const socket = net.connect(proxyPort, \"127.0.0.1\");\n\n    const { promise, resolve } = Promise.withResolvers<void>();\n\n    socket.on(\"connect\", () => {\n      socket.write(\n        \"GET / HTTP/1.1\\r\\n\" +\n          `Host: 127.0.0.1:${proxyPort}\\r\\n` +\n          \"Upgrade: websocket\\r\\n\" +\n          \"Connection: Upgrade\\r\\n\" +\n          \"\\r\\n\",\n      );\n    });\n\n    socket.on(\"error\", () => {\n      // Expected — connection closed\n    });\n\n    // Wait for the target response to arrive and verify no crash\n    setTimeout(() => {\n      target.close();\n      proxyServer.close();\n      proxy.close();\n      resolve();\n    }, 200);\n\n    await promise;\n  });\n\n  it(\"proxyUpgrade() should not crash when socket is destroyed before upstream responds\", async () => {\n    // Target server that responds with a normal HTTP 404 (no upgrade)\n    const target = http.createServer((_req, res) => {\n      setTimeout(() => {\n        res.writeHead(404);\n        res.end(\"Not Found\");\n      }, 50);\n    });\n    const targetPort = await listenOn(target);\n\n    const server = http.createServer();\n    server.on(\"upgrade\", (req, socket, head) => {\n      // Destroy the socket before the upstream response arrives\n      setTimeout(() => {\n        socket.destroy();\n      }, 10);\n      proxyUpgrade(`http://127.0.0.1:${targetPort}`, req, socket, head).catch(() => {\n        // Expected rejection — upstream didn't upgrade\n      });\n    });\n    const serverPort = await listenOn(server);\n\n    const socket = net.connect(serverPort, \"127.0.0.1\");\n\n    const { promise, resolve } = Promise.withResolvers<void>();\n\n    socket.on(\"connect\", () => {\n      socket.write(\n        \"GET / HTTP/1.1\\r\\n\" +\n          `Host: 127.0.0.1:${serverPort}\\r\\n` +\n          \"Upgrade: websocket\\r\\n\" +\n          \"Connection: Upgrade\\r\\n\" +\n          \"\\r\\n\",\n      );\n    });\n\n    socket.on(\"error\", () => {\n      // Expected — connection closed\n    });\n\n    setTimeout(() => {\n      target.close();\n      server.close();\n      resolve();\n    }, 200);\n\n    await promise;\n  });\n});\n"
  },
  {
    "path": "test/ws.test.ts",
    "content": "import { createServer, type Server, type IncomingMessage } from \"node:http\";\nimport { createServer as createHTTPSServer } from \"node:https\";\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { Duplex } from \"node:stream\";\nimport { connect, type AddressInfo } from \"node:net\";\nimport { afterAll, beforeAll, describe, expect, it } from \"vitest\";\nimport * as ws from \"ws\";\n\nimport type { ProxyAddr } from \"../src/types.ts\";\nimport { proxyUpgrade } from \"../src/ws.ts\";\n\n// --- WebSocket echo server ---\n\nlet wsServer: ws.WebSocketServer;\nlet httpServer: Server;\nlet wsPort: number;\n\nbeforeAll(async () => {\n  httpServer = createServer();\n  wsServer = new ws.WebSocketServer({ server: httpServer });\n\n  wsServer.on(\"connection\", (socket) => {\n    socket.on(\"message\", (msg) => {\n      socket.send(\"echo:\" + msg.toString(\"utf8\"));\n    });\n  });\n\n  await new Promise<void>((resolve) => {\n    httpServer.listen(0, \"127.0.0.1\", resolve);\n  });\n  wsPort = (httpServer.address() as AddressInfo).port;\n});\n\nafterAll(() => {\n  wsServer?.close();\n  httpServer?.close();\n});\n\n// --- Helper: create a local HTTP server that uses proxyUpgrade on upgrade ---\n\nfunction createProxyServer(addr: string | ProxyAddr, opts?: Parameters<typeof proxyUpgrade>[4]) {\n  const server = createServer((_req, res) => {\n    res.writeHead(404);\n    res.end();\n  });\n\n  server.on(\"upgrade\", (req, socket, head) => {\n    proxyUpgrade(addr, req, socket, head, opts).catch(() => {});\n  });\n\n  return server;\n}\n\nasync function listenServer(server: Server): Promise<number> {\n  await new Promise<void>((resolve) => {\n    server.listen(0, \"127.0.0.1\", resolve);\n  });\n  return (server.address() as AddressInfo).port;\n}\n\nfunction makeDummySocket() {\n  return new Duplex({\n    read() {},\n    write(_c, _e, cb) {\n      cb();\n    },\n  });\n}\n\nfunction wsUpgradeRequest(port: number): string {\n  return (\n    \"GET / HTTP/1.1\\r\\n\" +\n    `Host: 127.0.0.1:${port}\\r\\n` +\n    \"Upgrade: websocket\\r\\n\" +\n    \"Connection: Upgrade\\r\\n\" +\n    \"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\\r\\n\" +\n    \"Sec-WebSocket-Version: 13\\r\\n\" +\n    \"\\r\\n\"\n  );\n}\n\nasync function createTargetServer(\n  onUpgrade?: (req: IncomingMessage, socket: Duplex, head: Buffer) => void,\n): Promise<{ server: Server; port: number }> {\n  const server = createServer();\n  if (onUpgrade) server.on(\"upgrade\", onUpgrade);\n  const port = await listenServer(server);\n  return { server, port };\n}\n\n// --- Tests ---\n\ndescribe(\"proxyUpgrade\", () => {\n  describe(\"validate ws upgrade request\", () => {\n    it(\"rejects non-GET method\", async () => {\n      const socket = makeDummySocket();\n      const req = {\n        method: \"POST\",\n        headers: { upgrade: \"websocket\" },\n        socket: { remoteAddress: \"127.0.0.1\" },\n      } as any;\n      await expect(proxyUpgrade({ host: \"127.0.0.1\", port: 1 }, req, socket)).rejects.toThrow(\n        \"Not a valid WebSocket upgrade request\",\n      );\n      expect(socket.destroyed).toBe(true);\n    });\n\n    it(\"rejects missing upgrade header\", async () => {\n      const socket = makeDummySocket();\n      const req = { method: \"GET\", headers: {}, socket: { remoteAddress: \"127.0.0.1\" } } as any;\n      await expect(proxyUpgrade({ host: \"127.0.0.1\", port: 1 }, req, socket)).rejects.toThrow(\n        \"Not a valid WebSocket upgrade request\",\n      );\n      expect(socket.destroyed).toBe(true);\n    });\n\n    it(\"rejects non-websocket upgrade header\", async () => {\n      const socket = makeDummySocket();\n      const req = {\n        method: \"GET\",\n        headers: { upgrade: \"h2c\" },\n        socket: { remoteAddress: \"127.0.0.1\" },\n      } as any;\n      await expect(proxyUpgrade({ host: \"127.0.0.1\", port: 1 }, req, socket)).rejects.toThrow(\n        \"Not a valid WebSocket upgrade request\",\n      );\n      expect(socket.destroyed).toBe(true);\n    });\n  });\n\n  it(\"should proxy websocket messages\", async () => {\n    const proxy = createProxyServer({ host: \"127.0.0.1\", port: wsPort });\n    const proxyPort = await listenServer(proxy);\n\n    const { promise, resolve } = Promise.withResolvers<void>();\n    const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n    client.on(\"open\", () => {\n      client.send(\"hello\");\n    });\n\n    client.on(\"message\", (msg) => {\n      expect(msg.toString(\"utf8\")).toBe(\"echo:hello\");\n      client.close();\n      proxy.close(() => resolve());\n    });\n\n    await promise;\n  });\n\n  it(\"should accept URL string as addr\", async () => {\n    const proxy = createProxyServer(`ws://127.0.0.1:${wsPort}`);\n    const proxyPort = await listenServer(proxy);\n\n    const { promise, resolve } = Promise.withResolvers<void>();\n    const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n    client.on(\"open\", () => {\n      client.send(\"url-addr\");\n    });\n\n    client.on(\"message\", (msg) => {\n      expect(msg.toString(\"utf8\")).toBe(\"echo:url-addr\");\n      client.close();\n      proxy.close(() => resolve());\n    });\n\n    await promise;\n  });\n\n  it(\"should reject on connection error\", async () => {\n    const { promise, resolve } = Promise.withResolvers<void>();\n    const server = createServer();\n\n    server.on(\"upgrade\", (req, socket, head) => {\n      proxyUpgrade({ host: \"127.0.0.1\", port: 1 }, req, socket, head).catch((err) => {\n        expect(err).toBeDefined();\n        resolve();\n      });\n    });\n\n    const port = await listenServer(server);\n    const client = new ws.WebSocket(\"ws://127.0.0.1:\" + port);\n\n    client.on(\"error\", () => {\n      // Expected — upstream connection fails\n    });\n\n    await promise;\n    server.close();\n  });\n\n  it(\"should add x-forwarded headers when xfwd is set\", async () => {\n    const { server: targetServer, port: targetPort } = await createTargetServer();\n    const targetWs = new ws.WebSocketServer({ server: targetServer });\n\n    targetWs.on(\"connection\", (socket, req) => {\n      socket.send(\n        JSON.stringify({\n          \"x-forwarded-for\": req.headers[\"x-forwarded-for\"],\n          \"x-forwarded-port\": req.headers[\"x-forwarded-port\"],\n          \"x-forwarded-proto\": req.headers[\"x-forwarded-proto\"],\n        }),\n      );\n    });\n\n    const proxy = createProxyServer({ host: \"127.0.0.1\", port: targetPort }, { xfwd: true });\n    const proxyPort = await listenServer(proxy);\n\n    const { promise, resolve } = Promise.withResolvers<void>();\n    const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n    client.on(\"message\", (msg) => {\n      const headers = JSON.parse(msg.toString(\"utf8\"));\n      expect(headers[\"x-forwarded-for\"]).toBeDefined();\n      expect(headers[\"x-forwarded-port\"]).toBeDefined();\n      expect(headers[\"x-forwarded-proto\"]).toBe(\"ws\");\n      client.close();\n      targetWs.close();\n      targetServer.close();\n      proxy.close(() => resolve());\n    });\n\n    await promise;\n  });\n\n  it(\"should reject when upstream responds without upgrading\", async () => {\n    // Target is a plain HTTP server that never upgrades — just returns 404\n    const targetServer = createServer((_req, res) => {\n      res.writeHead(404);\n      res.end(\"Not Found\");\n    });\n    const targetPort = await listenServer(targetServer);\n\n    const server = createServer();\n    const { promise, resolve } = Promise.withResolvers<void>();\n\n    server.on(\"upgrade\", (req, socket, head) => {\n      proxyUpgrade({ host: \"127.0.0.1\", port: targetPort }, req, socket, head)\n        .then(() => {\n          expect.unreachable(\"should not resolve on non-upgrade response\");\n        })\n        .catch((err) => {\n          expect(err.message).toContain(\"did not upgrade the connection\");\n          resolve();\n        });\n    });\n\n    const port = await listenServer(server);\n    const client = new ws.WebSocket(\"ws://127.0.0.1:\" + port);\n    client.on(\"error\", () => {\n      // Expected — proxy relays the non-upgrade response\n    });\n\n    await promise;\n    targetServer.close();\n    server.close();\n  }, 5000);\n\n  it(\"should not write to socket when it closes before non-upgrade response\", async () => {\n    // Regression: upstream PR http-party/node-http-proxy#1552\n    // If the client socket is not writable when the upstream non-upgrade\n    // response arrives, socket.write() should be skipped to avoid\n    // \"This socket has been ended by the other party\" errors.\n    const { promise: targetReqReceived, resolve: onTargetReq } = Promise.withResolvers<void>();\n    const { promise: canRespond, resolve: allowResponse } = Promise.withResolvers<void>();\n\n    const targetServer = createServer(async (_req, res) => {\n      onTargetReq();\n      await canRespond;\n      res.writeHead(404);\n      res.end(\"Not Found\");\n    });\n    const targetPort = await listenServer(targetServer);\n\n    const server = createServer();\n    const { promise, resolve } = Promise.withResolvers<void>();\n    let socketWriteCalled = false;\n\n    server.on(\"upgrade\", (req, socket, head) => {\n      const origWrite = socket.write.bind(socket) as typeof socket.write;\n      socket.write = function (chunk: any, encodingOrCb?: any, cb?: any) {\n        socketWriteCalled = true;\n        return origWrite(chunk, encodingOrCb, cb);\n      } as typeof socket.write;\n\n      // Destroy the server-side socket before the upstream responds,\n      // simulating a client that disconnects and the server detects it.\n      targetReqReceived.then(() => {\n        socket.destroy();\n        // Allow the target to respond after the socket is destroyed\n        setTimeout(allowResponse, 10);\n      });\n\n      proxyUpgrade({ host: \"127.0.0.1\", port: targetPort }, req, socket, head).catch(() => {\n        resolve();\n      });\n    });\n\n    const port = await listenServer(server);\n\n    const rawSocket = connect(port, \"127.0.0.1\", () => {\n      rawSocket.write(wsUpgradeRequest(port));\n    });\n    rawSocket.on(\"error\", () => {});\n\n    await promise;\n    expect(socketWriteCalled).toBe(false);\n    targetServer.close();\n    server.close();\n  }, 5000);\n\n  it(\"should not crash when upstream response errors during non-upgrade pipe\", async () => {\n    // Regression: https://github.com/http-party/node-http-proxy/pull/1439\n    // When upstream returns a non-upgrade response and the response stream errors\n    // (e.g., ECONNRESET), the error on `res` was unhandled and crashed the process.\n    const targetServer = createServer((req, res) => {\n      res.writeHead(502);\n      res.write(\"partial\");\n      setTimeout(() => req.socket.destroy(), 10);\n    });\n    const targetPort = await listenServer(targetServer);\n\n    const server = createServer();\n    const { promise, resolve } = Promise.withResolvers<void>();\n\n    server.on(\"upgrade\", (req, socket, head) => {\n      proxyUpgrade({ host: \"127.0.0.1\", port: targetPort }, req, socket, head).catch(() => {\n        resolve();\n      });\n    });\n\n    const port = await listenServer(server);\n    const client = new ws.WebSocket(\"ws://127.0.0.1:\" + port);\n    client.on(\"error\", () => {});\n\n    await promise;\n    targetServer.close();\n    server.close();\n  }, 5000);\n\n  it(\"should not emit undefined header values\", async () => {\n    const { server: targetServer, port: targetPort } = await createTargetServer((req, socket) => {\n      socket.write(\n        \"HTTP/1.1 101 Switching Protocols\\r\\n\" +\n          \"Upgrade: websocket\\r\\n\" +\n          \"Connection: Upgrade\\r\\n\" +\n          \"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\\r\\n\" +\n          \"\\r\\n\",\n      );\n      req.socket.pipe(socket).pipe(req.socket);\n    });\n\n    const proxy = createProxyServer({ host: \"127.0.0.1\", port: targetPort });\n    const proxyPort = await listenServer(proxy);\n\n    const { promise, resolve } = Promise.withResolvers<void>();\n\n    const sock = connect(proxyPort, \"127.0.0.1\", () => {\n      sock.write(wsUpgradeRequest(proxyPort));\n    });\n\n    sock.on(\"data\", (data) => {\n      const response = data.toString();\n      // The response headers should never contain literal \"undefined\"\n      expect(response).not.toContain(\": undefined\");\n      sock.destroy();\n      targetServer.close();\n      proxy.close(() => resolve());\n    });\n\n    await promise;\n  });\n\n  it(\"should preserve multiple set-cookie headers in upgrade response\", async () => {\n    const { server: targetServer, port: targetPort } = await createTargetServer((req, socket) => {\n      socket.write(\n        \"HTTP/1.1 101 Switching Protocols\\r\\n\" +\n          \"Upgrade: websocket\\r\\n\" +\n          \"Connection: Upgrade\\r\\n\" +\n          \"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\\r\\n\" +\n          \"Set-Cookie: a=1\\r\\n\" +\n          \"Set-Cookie: b=2\\r\\n\" +\n          \"Set-Cookie: c=3\\r\\n\" +\n          \"\\r\\n\",\n      );\n      req.socket.pipe(socket).pipe(req.socket);\n    });\n\n    const proxy = createProxyServer({ host: \"127.0.0.1\", port: targetPort });\n    const proxyPort = await listenServer(proxy);\n\n    const { promise, resolve } = Promise.withResolvers<void>();\n\n    const sock = connect(proxyPort, \"127.0.0.1\", () => {\n      sock.write(wsUpgradeRequest(proxyPort));\n    });\n\n    sock.on(\"data\", (data) => {\n      const response = data.toString();\n      // All three Set-Cookie values must appear as separate headers\n      const cookies = [...response.matchAll(/set-cookie: (.+)/gi)].map((m) => m[1]!.trim());\n      expect(cookies).toContain(\"a=1\");\n      expect(cookies).toContain(\"b=2\");\n      expect(cookies).toContain(\"c=3\");\n      sock.destroy();\n      targetServer.close();\n      proxy.close(() => resolve());\n    });\n\n    await promise;\n  });\n\n  it(\"should resolve with the proxy socket\", async () => {\n    const server = createServer();\n\n    const { promise, resolve } = Promise.withResolvers<void>();\n\n    server.on(\"upgrade\", async (req, socket, head) => {\n      const proxySocket = await proxyUpgrade(\n        { host: \"127.0.0.1\", port: wsPort },\n        req,\n        socket,\n        head,\n      );\n      expect(proxySocket).toBeDefined();\n      expect(proxySocket.writable).toBe(true);\n      resolve();\n    });\n\n    const port = await listenServer(server);\n    const client = new ws.WebSocket(\"ws://127.0.0.1:\" + port);\n\n    client.on(\"open\", () => {\n      client.close();\n    });\n\n    await promise;\n    server.close();\n  });\n\n  it(\"should forward non-empty head buffer to upstream\", async () => {\n    const received: Buffer[] = [];\n    const { promise, resolve } = Promise.withResolvers<void>();\n\n    const { server: targetServer, port: targetPort } = await createTargetServer((_req, socket) => {\n      socket.write(\n        \"HTTP/1.1 101 Switching Protocols\\r\\n\" +\n          \"Upgrade: websocket\\r\\n\" +\n          \"Connection: Upgrade\\r\\n\" +\n          \"\\r\\n\",\n      );\n      socket.on(\"data\", (chunk: Buffer) => {\n        received.push(Buffer.from(chunk));\n        setTimeout(() => socket.destroy(), 20);\n      });\n      socket.on(\"close\", () => {\n        const all = Buffer.concat(received);\n        expect(all.toString()).toContain(\"head-payload-data\");\n        resolve();\n      });\n    });\n\n    // Proxy server that injects a non-empty head buffer\n    const server = createServer();\n\n    server.on(\"upgrade\", (req, socket, _head) => {\n      const syntheticHead = Buffer.from(\"head-payload-data\");\n      proxyUpgrade({ host: \"127.0.0.1\", port: targetPort }, req, socket, syntheticHead).catch(\n        () => {},\n      );\n    });\n\n    const port = await listenServer(server);\n\n    const sock = connect(port, \"127.0.0.1\", () => {\n      sock.end(wsUpgradeRequest(port));\n    });\n\n    sock.on(\"error\", () => {});\n\n    await promise;\n    targetServer.close();\n    server.close();\n  });\n\n  describe(\"disconnect scenarios\", () => {\n    it(\"client socket error before upgrade rejects and destroys proxy request\", async () => {\n      const { server: targetServer, port: targetPort } = await createTargetServer(() => {\n        // Intentionally hang — never respond\n      });\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      const server = createServer();\n\n      server.on(\"upgrade\", (req, socket, head) => {\n        const result = proxyUpgrade({ host: \"127.0.0.1\", port: targetPort }, req, socket, head);\n\n        // Emit an error on the client socket after a short delay\n        // (upstream upgrade is still pending)\n        setTimeout(() => {\n          socket.destroy(new Error(\"client socket error\"));\n        }, 30);\n\n        result\n          .then(() => {\n            expect.unreachable(\"should not resolve when client socket errors\");\n          })\n          .catch((err) => {\n            expect(err.message).toBe(\"client socket error\");\n            resolve();\n          });\n      });\n\n      const port = await listenServer(server);\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + port);\n      client.on(\"error\", () => {\n        // Expected\n      });\n\n      await promise;\n      targetServer.close();\n      server.close();\n    });\n\n    it(\"client disconnect before upgrade rejects the promise\", async () => {\n      const targetWs = new ws.WebSocketServer({ noServer: true });\n\n      const { server: targetServer, port: targetPort } = await createTargetServer(\n        (req, socket, head) => {\n          setTimeout(() => {\n            targetWs.handleUpgrade(req, socket as any, head, (client) => {\n              targetWs.emit(\"connection\", client, req);\n            });\n          }, 200);\n        },\n      );\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      const server = createServer();\n\n      server.on(\"upgrade\", (req, socket, head) => {\n        proxyUpgrade({ host: \"127.0.0.1\", port: targetPort }, req, socket, head)\n          .then(() => {\n            expect.unreachable(\"should not resolve when client disconnects early\");\n          })\n          .catch((err) => {\n            expect(err).toBeDefined();\n            resolve();\n          });\n      });\n\n      const port = await listenServer(server);\n\n      const sock = connect(port, \"127.0.0.1\", () => {\n        sock.write(wsUpgradeRequest(port));\n        setTimeout(() => sock.destroy(), 50);\n      });\n\n      await promise;\n      targetWs.close();\n      targetServer.close();\n      server.close();\n    });\n\n    it(\"client disconnect after upgrade ends the upstream socket\", async () => {\n      const { server: targetServer, port: targetPort } = await createTargetServer();\n      const targetWs = new ws.WebSocketServer({ server: targetServer });\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n\n      targetWs.on(\"connection\", (socket) => {\n        socket.on(\"close\", () => resolve());\n      });\n\n      const proxy = createProxyServer({ host: \"127.0.0.1\", port: targetPort });\n      const proxyPort = await listenServer(proxy);\n\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n      client.on(\"open\", () => {\n        // Forcefully terminate the client connection\n        client.terminate();\n      });\n\n      await promise;\n      targetWs.close();\n      targetServer.close();\n      proxy.close();\n    });\n\n    it(\"client socket error after upgrade ends the upstream proxy socket\", async () => {\n      const { server: targetServer, port: targetPort } = await createTargetServer();\n      const targetWs = new ws.WebSocketServer({ server: targetServer });\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n\n      const server = createServer();\n\n      server.on(\"upgrade\", async (req, socket, head) => {\n        const proxySocket = await proxyUpgrade(\n          { host: \"127.0.0.1\", port: targetPort },\n          req,\n          socket,\n          head,\n        );\n\n        proxySocket.on(\"close\", () => {\n          resolve();\n        });\n\n        // Emit an error on the client socket after upgrade is complete\n        // This triggers the post-upgrade error handler: sock.once(\"error\", () => { proxySocket.end() })\n        socket.destroy(new Error(\"post-upgrade client error\"));\n      });\n\n      const port = await listenServer(server);\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + port);\n      client.on(\"error\", () => {});\n\n      await promise;\n      targetWs.close();\n      targetServer.close();\n      server.close();\n    });\n\n    it(\"server disconnect during upgrade rejects the promise\", async () => {\n      const { server: targetServer, port: targetPort } = await createTargetServer(\n        (_req, socket) => {\n          socket.destroy();\n        },\n      );\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      const server = createServer();\n\n      server.on(\"upgrade\", (req, socket, head) => {\n        proxyUpgrade({ host: \"127.0.0.1\", port: targetPort }, req, socket, head)\n          .then(() => {\n            expect.unreachable(\"should not resolve when server destroys during upgrade\");\n          })\n          .catch((err) => {\n            expect(err).toBeDefined();\n            resolve();\n          });\n      });\n\n      const port = await listenServer(server);\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + port);\n      client.on(\"error\", () => {\n        // Expected\n      });\n\n      await promise;\n      targetServer.close();\n      server.close();\n    });\n\n    it(\"server disconnect after upgrade ends the client socket\", async () => {\n      const { server: targetServer, port: targetPort } = await createTargetServer();\n      const targetWs = new ws.WebSocketServer({ server: targetServer });\n\n      targetWs.on(\"connection\", (socket) => {\n        setTimeout(() => socket.terminate(), 50);\n      });\n\n      const proxy = createProxyServer({ host: \"127.0.0.1\", port: targetPort });\n      const proxyPort = await listenServer(proxy);\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n      client.on(\"close\", () => {\n        // Client sees the close after server disconnects\n        resolve();\n      });\n\n      client.on(\"error\", () => {\n        // May fire before close on some platforms\n      });\n\n      await promise;\n      targetWs.close();\n      targetServer.close();\n      proxy.close();\n    });\n  });\n\n  describe(\"wss:// (TLS upstream)\", () => {\n    const __dirname = new URL(\".\", import.meta.url).pathname;\n    const sslOpts = {\n      key: readFileSync(join(__dirname, \"fixtures\", \"agent2-key.pem\")),\n      cert: readFileSync(join(__dirname, \"fixtures\", \"agent2-cert.pem\")),\n    };\n\n    it(\"should use HTTPS request for wss:// addr\", async () => {\n      // Create an HTTPS server with WebSocket support\n      const httpsServer = createHTTPSServer(sslOpts);\n      const targetWs = new ws.WebSocketServer({ server: httpsServer });\n\n      targetWs.on(\"connection\", (socket) => {\n        socket.on(\"message\", (msg) => {\n          socket.send(\"secure-echo:\" + msg.toString(\"utf8\"));\n        });\n      });\n\n      await new Promise<void>((resolve) => {\n        httpsServer.listen(0, \"127.0.0.1\", resolve);\n      });\n      const targetPort = (httpsServer.address() as AddressInfo).port;\n\n      const proxy = createProxyServer(`wss://127.0.0.1:${targetPort}`, {\n        secure: false,\n      });\n      const proxyPort = await listenServer(proxy);\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n      client.on(\"open\", () => {\n        client.send(\"tls-test\");\n      });\n\n      client.on(\"message\", (msg) => {\n        expect(msg.toString(\"utf8\")).toBe(\"secure-echo:tls-test\");\n        client.close();\n        targetWs.close();\n        httpsServer.close();\n        proxy.close(() => resolve());\n      });\n\n      client.on(\"error\", (err) => {\n        targetWs.close();\n        httpsServer.close();\n        proxy.close();\n        throw err;\n      });\n\n      await promise;\n    });\n\n    it(\"should use HTTPS request for https:// addr\", async () => {\n      const httpsServer = createHTTPSServer(sslOpts);\n      const targetWs = new ws.WebSocketServer({ server: httpsServer });\n\n      targetWs.on(\"connection\", (socket) => {\n        socket.on(\"message\", (msg) => {\n          socket.send(\"https-echo:\" + msg.toString(\"utf8\"));\n        });\n      });\n\n      await new Promise<void>((resolve) => {\n        httpsServer.listen(0, \"127.0.0.1\", resolve);\n      });\n      const targetPort = (httpsServer.address() as AddressInfo).port;\n\n      const proxy = createProxyServer(`https://127.0.0.1:${targetPort}`, {\n        secure: false,\n      });\n      const proxyPort = await listenServer(proxy);\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n      client.on(\"open\", () => {\n        client.send(\"https-test\");\n      });\n\n      client.on(\"message\", (msg) => {\n        expect(msg.toString(\"utf8\")).toBe(\"https-echo:https-test\");\n        client.close();\n        targetWs.close();\n        httpsServer.close();\n        proxy.close(() => resolve());\n      });\n\n      client.on(\"error\", (err) => {\n        targetWs.close();\n        httpsServer.close();\n        proxy.close();\n        throw err;\n      });\n\n      await promise;\n    });\n\n    it(\"should use plain HTTP request for ws:// addr (no TLS)\", async () => {\n      // Verify ws:// still uses plain HTTP (not HTTPS)\n      const proxy = createProxyServer({ host: \"127.0.0.1\", port: wsPort });\n      const proxyPort = await listenServer(proxy);\n\n      const { promise, resolve } = Promise.withResolvers<void>();\n      const client = new ws.WebSocket(\"ws://127.0.0.1:\" + proxyPort);\n\n      client.on(\"open\", () => {\n        client.send(\"plain-test\");\n      });\n\n      client.on(\"message\", (msg) => {\n        expect(msg.toString(\"utf8\")).toBe(\"echo:plain-test\");\n        client.close();\n        proxy.close(() => resolve());\n      });\n\n      await promise;\n    });\n  });\n\n  describe(\"non-upgrade response with destroyed socket\", () => {\n    it(\"should consume response body when socket is already destroyed\", async () => {\n      // Regression: when the client socket is destroyed before the upstream\n      // non-upgrade response arrives, the response stream must be consumed\n      // (res.resume()) to avoid unhandled stream errors.\n      const { promise: targetReqReceived, resolve: onTargetReq } = Promise.withResolvers<void>();\n      const { promise: canRespond, resolve: allowResponse } = Promise.withResolvers<void>();\n\n      const targetServer = createServer(async (_req, res) => {\n        onTargetReq();\n        await canRespond;\n        // Send a larger response body to make unconsumed stream errors more likely\n        res.writeHead(503);\n        res.end(\"Service Unavailable — \" + \"x\".repeat(1024));\n      });\n      const targetPort = await listenServer(targetServer);\n\n      const server = createServer();\n      const { promise, resolve } = Promise.withResolvers<void>();\n\n      server.on(\"upgrade\", (req, socket, head) => {\n        // Destroy socket before upstream responds\n        targetReqReceived.then(() => {\n          socket.destroy();\n          setTimeout(allowResponse, 10);\n        });\n\n        proxyUpgrade({ host: \"127.0.0.1\", port: targetPort }, req, socket, head).catch(() => {\n          // Give time for any potential unhandled stream errors to surface\n          setTimeout(resolve, 50);\n        });\n      });\n\n      const port = await listenServer(server);\n\n      const sock = connect(port, \"127.0.0.1\", () => {\n        sock.write(wsUpgradeRequest(port));\n      });\n      sock.on(\"error\", () => {});\n\n      await promise;\n      targetServer.close();\n      server.close();\n    });\n  });\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"module\": \"nodenext\",\n    \"moduleResolution\": \"nodenext\",\n    \"moduleDetection\": \"force\",\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"isolatedModules\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"allowImportingTsExtensions\": true,\n    \"noImplicitOverride\": true,\n    \"noEmit\": true\n  }\n}\n"
  },
  {
    "path": "vitest.config.mjs",
    "content": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    typecheck: { enabled: true },\n    coverage: {\n      include: [\"src/**/*.ts\"],\n    },\n  },\n});\n"
  }
]