diff --git a/.agents/skills/javascript-proxy-headers/SKILL.md b/.agents/skills/javascript-proxy-headers/SKILL.md index 1a252af..07dcf79 100644 --- a/.agents/skills/javascript-proxy-headers/SKILL.md +++ b/.agents/skills/javascript-proxy-headers/SKILL.md @@ -46,7 +46,7 @@ const client = createProxyAxios({ }); const response = await client.get('https://httpbin.org/ip'); -console.log(response.headers['x-proxymesh-ip']); +console.log(response.proxyHeaders.get('x-proxymesh-ip')); ``` ### node-fetch @@ -73,7 +73,7 @@ const client = createProxyGot({ }); const response = await client('https://httpbin.org/ip'); -console.log(response.headers['x-proxymesh-ip']); +console.log(response.proxyHeaders.get('x-proxymesh-ip')); ``` ### undici diff --git a/README.md b/README.md index 594c44c..8f062c6 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,20 @@ Then install the HTTP client(s) you use (for example `axios`, `got`, `ky`, `wret > **Note:** This package has no runtime dependencies by default—install only the adapters you need. +## 0.3.0 breaking changes + +CONNECT response headers are **not** copied onto origin `response.headers`. Read them from the per-request `proxyHeaders` Map so concurrent requests stay isolated and hop-by-hop CONNECT headers cannot impersonate origin headers (`Set-Cookie`, `Location`, and similar). + +```javascript +// ≤0.2.x (removed) +response.headers['x-proxymesh-ip'] + +// 0.3.x +response.proxyHeaders.get('x-proxymesh-ip') +``` + +`agent.lastProxyHeaders` still exists as a last-write-wins snapshot. Prefer `response.proxyHeaders` (or `getProxyHeaders()`) under concurrency. + ## Quick Start ### axios @@ -54,8 +68,7 @@ const client = createProxyAxios({ const response = await client.get('https://httpbin.org/ip'); -// Proxy headers are merged into response.headers -console.log(response.headers['x-proxymesh-ip']); +console.log(response.proxyHeaders.get('x-proxymesh-ip')); ``` ### node-fetch @@ -83,7 +96,7 @@ const client = createProxyGot({ }); const response = await client('https://httpbin.org/ip'); -console.log(response.headers['x-proxymesh-ip']); +console.log(response.proxyHeaders.get('x-proxymesh-ip')); ``` ### undici @@ -160,8 +173,7 @@ const res = await proxyNeedleGet('https://httpbin.org/ip', { proxyHeaders: { 'X-ProxyMesh-Country': 'US' } }); -// CONNECT response headers merged onto res.headers where missing -console.log(res.headers['x-proxymesh-ip']); +console.log(res.proxyHeaders.get('x-proxymesh-ip')); ``` ### typed-rest-client @@ -177,8 +189,9 @@ const client = createProxyRestClient({ proxyHeaders: { 'X-ProxyMesh-Country': 'US' } }); -await client.get('https://httpbin.org/ip'); -console.log(client.proxyAgent.lastProxyHeaders?.get('x-proxymesh-ip')); +const response = await client.get('https://httpbin.org/ip'); +console.log(response.result); +console.log(response.proxyHeaders?.get('x-proxymesh-ip')); ``` ### Core Agent (Advanced) @@ -186,7 +199,7 @@ console.log(client.proxyAgent.lastProxyHeaders?.get('x-proxymesh-ip')); For direct control, use the core `ProxyHeadersAgent`: ```javascript -import { ProxyHeadersAgent } from 'javascript-proxy-headers'; +import { ProxyHeadersAgent, getProxyHeaders } from 'javascript-proxy-headers'; import https from 'https'; const agent = new ProxyHeadersAgent('http://proxy.example.com:8080', { @@ -197,7 +210,7 @@ const agent = new ProxyHeadersAgent('http://proxy.example.com:8080', { }); https.get('https://httpbin.org/ip', { agent }, (res) => { - // Handle response + console.log(getProxyHeaders(res)?.get('x-proxymesh-ip')); }); ``` diff --git a/docs/axios.md b/docs/axios.md index b293ef9..fc72057 100644 --- a/docs/axios.md +++ b/docs/axios.md @@ -29,8 +29,8 @@ const response = await client.get('https://httpbin.org/ip'); // Access response data console.log(response.data); -// Access proxy response headers (merged into response.headers) -console.log(response.headers['x-proxymesh-ip']); +// Access proxy CONNECT headers (not merged into origin response.headers) +console.log(response.proxyHeaders.get('x-proxymesh-ip')); ``` ## API Reference @@ -93,29 +93,16 @@ const response = await post('https://httpbin.org/post', { key: 'value' }, { ## Accessing Proxy Headers -Proxy response headers are automatically merged into `response.headers`: +CONNECT response headers are on `response.proxyHeaders` (a `Map`). They are **not** copied onto origin `response.headers`. ```javascript const response = await client.get('https://httpbin.org/ip'); -// Proxy headers are available in response.headers -const proxyIp = response.headers['x-proxymesh-ip']; -const proxyCountry = response.headers['x-proxymesh-country']; +const proxyIp = response.proxyHeaders.get('x-proxymesh-ip'); +const proxyCountry = response.proxyHeaders.get('x-proxymesh-country'); ``` -You can also access the underlying agent to get the last proxy headers: - -```javascript -const client = await createProxyAxios({ - proxy: 'http://proxy:8080', - proxyHeaders: { 'X-ProxyMesh-Country': 'US' } -}); - -await client.get('https://httpbin.org/ip'); - -// Access via the agent -console.log(client.proxyAgent.lastProxyHeaders); -``` +`client.proxyAgent.lastProxyHeaders` is a last-write-wins snapshot of the most recent CONNECT. Use `response.proxyHeaders` for concurrent requests. ## All Request Methods diff --git a/docs/core-api.md b/docs/core-api.md index 160ae2b..a4f55eb 100644 --- a/docs/core-api.md +++ b/docs/core-api.md @@ -32,7 +32,7 @@ new ProxyHeadersAgent(proxy, options) ### Example ```javascript -import { ProxyHeadersAgent } from 'javascript-proxy-headers'; +import { ProxyHeadersAgent, getProxyHeaders } from 'javascript-proxy-headers'; import https from 'https'; const agent = new ProxyHeadersAgent('http://proxy.example.com:8080', { @@ -56,7 +56,7 @@ const req = https.request({ res.on('data', chunk => body += chunk); res.on('end', () => { console.log(body); - console.log('Last proxy headers:', agent.lastProxyHeaders); + console.log('CONNECT headers:', getProxyHeaders(res)); }); }); @@ -72,7 +72,7 @@ req.end(); | `proxyAuth` | `string \| null` | Base64-encoded proxy auth | | `proxyProtocol` | `string` | Proxy URL protocol (`http:` or `https:`) | | `proxyHeaders` | `Object` | Headers to send to proxy | -| `lastProxyHeaders` | `Map \| null` | Headers from last CONNECT response | +| `lastProxyHeaders` | `Map \| null` | Most recent CONNECT headers (last-write-wins under concurrency) | ## ConnectError @@ -184,6 +184,20 @@ const response = parseConnectResponse(buffer); // } ``` +### getProxyHeaders(source) + +Return the CONNECT `Map` attached to a TLS socket, Node `IncomingMessage`, or HTTP client response. Use this instead of `agent.lastProxyHeaders` when requests may overlap. + +```javascript +import { ProxyHeadersAgent, getProxyHeaders } from 'javascript-proxy-headers'; +import https from 'https'; + +const agent = new ProxyHeadersAgent('http://proxy:8080'); +https.get('https://example.com/', { agent }, (res) => { + console.log(getProxyHeaders(res)?.get('x-proxymesh-ip')); +}); +``` + ## Using with Other Libraries The core agent can be used with any library that accepts an `https.Agent`: @@ -205,10 +219,10 @@ import { fetch, setGlobalDispatcher, Agent } from 'undici'; ### With needle -For normal use, prefer the [needle adapter](needle.md) (`proxyNeedleGet` / `createProxyNeedle`), which merges CONNECT headers onto the response. To wire the agent yourself: +For normal use, prefer the [needle adapter](needle.md) (`proxyNeedleGet` / `createProxyNeedle`), which exposes CONNECT headers on `res.proxyHeaders`. To wire the agent yourself: ```javascript -import { ProxyHeadersAgent } from 'javascript-proxy-headers'; +import { ProxyHeadersAgent, getProxyHeaders } from 'javascript-proxy-headers'; import needle from 'needle'; const agent = new ProxyHeadersAgent('http://proxy:8080', { @@ -217,7 +231,7 @@ const agent = new ProxyHeadersAgent('http://proxy:8080', { needle.get('https://httpbin.org/ip', { agent }, (err, resp) => { console.log(resp.body); - console.log(agent.lastProxyHeaders); + console.log(getProxyHeaders(resp)?.get('x-proxymesh-ip')); }); ``` diff --git a/docs/getting-started.md b/docs/getting-started.md index b88ccb6..c7cea8b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -28,6 +28,12 @@ npm install typed-rest-client Use [ky](ky.md) or [wretch](wretch.md) together with `node-fetch` (the adapters build on the same proxy-aware fetch as the node-fetch module). +## 0.3.0 breaking changes + +CONNECT headers are no longer merged into origin `response.headers`. Use `response.proxyHeaders.get('x-proxymesh-ip')` instead. That keeps CONNECT metadata off the origin response (so `Set-Cookie` / `Location` from the proxy hop cannot impersonate the target) and keeps concurrent requests isolated. + +`agent.lastProxyHeaders` is last-write-wins. Prefer per-response `proxyHeaders` or `getProxyHeaders()`. + ## Quick Examples ### axios @@ -42,7 +48,7 @@ const client = await createProxyAxios({ const response = await client.get('https://httpbin.org/ip'); console.log(response.data); -console.log(response.headers['x-proxymesh-ip']); +console.log(response.proxyHeaders.get('x-proxymesh-ip')); ``` ### node-fetch @@ -72,7 +78,7 @@ const client = await createProxyGot({ const response = await client('https://httpbin.org/ip'); console.log(response.body); -console.log(response.headers['x-proxymesh-ip']); +console.log(response.proxyHeaders.get('x-proxymesh-ip')); ``` ### undici @@ -105,7 +111,7 @@ const client = await createProxySuperagent({ const response = await client.get('https://httpbin.org/ip'); console.log(response.body); -console.log(response.headers['x-proxymesh-ip']); +console.log(response.proxyHeaders.get('x-proxymesh-ip')); ``` ### ky @@ -165,7 +171,7 @@ const res = await proxyNeedleGet('https://httpbin.org/ip', { }); console.log(res.body); -console.log(res.headers['x-proxymesh-ip']); +console.log(res.proxyHeaders.get('x-proxymesh-ip')); ``` ### typed-rest-client @@ -179,8 +185,8 @@ const client = createProxyRestClient({ proxyHeaders: { 'X-ProxyMesh-Country': 'US' }, }); -await client.get('https://httpbin.org/ip'); -console.log(client.proxyAgent.lastProxyHeaders?.get('x-proxymesh-ip')); +const response = await client.get('https://httpbin.org/ip'); +console.log(response.proxyHeaders?.get('x-proxymesh-ip')); ``` ## Understanding Proxy Headers @@ -209,15 +215,16 @@ Proxy response headers from the CONNECT request are captured and made available. | Library | Access Method | |---------|---------------| -| axios | `response.headers['header-name']` (merged) | +| axios | `response.proxyHeaders.get('header-name')` | | node-fetch | `response.proxyHeaders.get('header-name')` | -| got | `response.headers['header-name']` (merged) | +| got | `response.proxyHeaders.get('header-name')` | | undici | `proxyHeaders.get('header-name')` | -| superagent | `response.headers['header-name']` (merged) | +| superagent | `response.proxyHeaders.get('header-name')` | | ky / wretch | `response.proxyHeaders.get('header-name')` on the fetch `Response` | | make-fetch-happen | `response.proxyHeaders.get('header-name')` | -| needle | `res.headers['header-name']` (merged where not already set) | -| typed-rest-client | `client.proxyAgent.lastProxyHeaders.get('header-name')` | +| needle | `res.proxyHeaders.get('header-name')` | +| typed-rest-client | `response.proxyHeaders.get('header-name')` (or `getProxyHeaders(httpResponse.message)`) | +| core `https.Agent` | `getProxyHeaders(incomingMessage)` or `onProxyConnect` | ## Proxy Authentication diff --git a/docs/got.md b/docs/got.md index d933f93..6e39ee3 100644 --- a/docs/got.md +++ b/docs/got.md @@ -27,8 +27,8 @@ const response = await client('https://httpbin.org/ip'); // Access response data console.log(response.body); -// Access proxy response headers (merged into response.headers) -console.log(response.headers['x-proxymesh-ip']); +// Access proxy CONNECT headers (not merged into origin response.headers) +console.log(response.proxyHeaders.get('x-proxymesh-ip')); ``` ## API Reference @@ -89,15 +89,12 @@ const response = await proxyPost('https://httpbin.org/post', { ## Accessing Proxy Headers -Proxy response headers are merged into `response.headers` and also available via `response.proxyHeaders`: +CONNECT response headers are on `response.proxyHeaders` (a `Map`). They are **not** copied onto origin `response.headers`. ```javascript const response = await client('https://httpbin.org/ip'); -// Merged into response.headers -const proxyIp = response.headers['x-proxymesh-ip']; - -// Also available separately +const proxyIp = response.proxyHeaders.get('x-proxymesh-ip'); const proxyHeaders = response.proxyHeaders; // Map ``` diff --git a/docs/index.md b/docs/index.md index 3d95524..19b69bf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,8 +66,7 @@ const client = await createProxyAxios({ const response = await client.get('https://httpbin.org/ip'); -// Proxy headers are merged into response.headers -console.log(response.headers['x-proxymesh-ip']); +console.log(response.proxyHeaders.get('x-proxymesh-ip')); ``` See the [Getting Started](getting-started.md) guide for more examples. diff --git a/docs/make-fetch-happen.md b/docs/make-fetch-happen.md index 512913c..37ddf91 100644 --- a/docs/make-fetch-happen.md +++ b/docs/make-fetch-happen.md @@ -30,7 +30,7 @@ console.log(response.proxyHeaders.get('x-proxymesh-ip')); ### createProxyMakeFetchHappen(options) -Builds `make-fetch-happen` with a `ProxyHeadersAgent`, then wraps the fetch so each response includes `proxyHeaders` from the last CONNECT. +Builds `make-fetch-happen` with a `ProxyHeadersAgent`, then wraps the fetch so each response includes `proxyHeaders` from **that request's** CONNECT. **Parameters:** @@ -44,7 +44,7 @@ Builds `make-fetch-happen` with a `ProxyHeadersAgent`, then wraps the fetch so e **Returns:** A `fetch` function with: - `.defaults(url, opts)` — same pattern as make-fetch-happen, still wrapped with `ProxyResponse` -- `.proxyAgent` — the `ProxyHeadersAgent` instance (for example `fetch.proxyAgent.lastProxyHeaders`) +- `.proxyAgent` — the `ProxyHeadersAgent` instance **Example:** @@ -67,7 +67,7 @@ console.log(res.proxyHeaders.get('x-proxymesh-ip')); ## Accessing Proxy Headers -Use `response.proxyHeaders.get('x-proxymesh-ip')` on the wrapped response, or read `fetch.proxyAgent.lastProxyHeaders` after a request. +Use `response.proxyHeaders.get('x-proxymesh-ip')` on the wrapped response. `fetch.proxyAgent.lastProxyHeaders` is last-write-wins and is not safe under concurrency. ## Synchronous Factory diff --git a/docs/needle.md b/docs/needle.md index afb0390..8bffa44 100644 --- a/docs/needle.md +++ b/docs/needle.md @@ -1,6 +1,6 @@ # needle -[needle](https://github.com/tomas/needle) is a lean HTTP client for Node. This package routes HTTPS through `ProxyHeadersAgent` and merges CONNECT response headers into the needle response where the same keys are not already set. +[needle](https://github.com/tomas/needle) is a lean HTTP client for Node. This package routes HTTPS through `ProxyHeadersAgent` and exposes CONNECT response headers on `res.proxyHeaders`. ## Getting Started @@ -21,9 +21,7 @@ const res = await proxyNeedleGet('https://httpbin.org/ip', { }); console.log(res.body); -// CONNECT headers merged into res.headers when missing -console.log(res.headers['x-proxymesh-ip']); -console.log(res.proxyAgent.lastProxyHeaders); +console.log(res.proxyHeaders.get('x-proxymesh-ip')); ``` ## API Reference @@ -67,7 +65,7 @@ const res = await get('https://httpbin.org/ip'); ## Accessing Proxy Headers -Prefer `res.headers['x-proxymesh-ip']` after merge, or `res.proxyAgent.lastProxyHeaders` for the raw `Map` from the last CONNECT. +Use `res.proxyHeaders.get('x-proxymesh-ip')`. CONNECT headers are not merged into origin `res.headers`. `res.proxyAgent.lastProxyHeaders` is a last-write-wins snapshot of the shared agent. ## Core Agent diff --git a/docs/superagent.md b/docs/superagent.md index 23f64cc..aa36bad 100644 --- a/docs/superagent.md +++ b/docs/superagent.md @@ -26,9 +26,7 @@ const response = await client.get('https://httpbin.org/ip'); // Access response data console.log(response.body); - -// Access proxy response headers (merged into response.headers) -console.log(response.headers['x-proxymesh-ip']); +console.log(response.proxyHeaders.get('x-proxymesh-ip')); ``` ## API Reference @@ -91,20 +89,17 @@ const response = await superagent })); console.log(response.body); -console.log(response.headers['x-proxymesh-ip']); +console.log(response.proxyHeaders.get('x-proxymesh-ip')); ``` ## Accessing Proxy Headers -Proxy response headers are merged into `response.headers` and also available via `response.proxyHeaders`: +CONNECT response headers are on `response.proxyHeaders` (a `Map`). They are **not** copied onto origin `response.headers`. ```javascript const response = await client.get('https://httpbin.org/ip'); -// Merged into response.headers -const proxyIp = response.headers['x-proxymesh-ip']; - -// Also available separately (Map) +const proxyIp = response.proxyHeaders.get('x-proxymesh-ip'); const proxyHeaders = response.proxyHeaders; ``` diff --git a/docs/testing.md b/docs/testing.md index 70a0934..44aefa9 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -190,9 +190,7 @@ const client = await createProxyAxios({ }); ``` -### Check Last Proxy Headers - -After a request, check the agent's `lastProxyHeaders`: +### Check Per-Response Proxy Headers ```javascript const client = await createProxyAxios({ @@ -200,11 +198,12 @@ const client = await createProxyAxios({ proxyHeaders: { 'X-ProxyMesh-Country': 'US' } }); -await client.get('https://httpbin.org/ip'); - -console.log('Last proxy headers:', client.proxyAgent.lastProxyHeaders); +const response = await client.get('https://httpbin.org/ip'); +console.log('CONNECT headers:', response.proxyHeaders); ``` +`client.proxyAgent.lastProxyHeaders` is only a last-write-wins snapshot. Use `response.proxyHeaders` when requests may overlap. + ## Common Issues ### "Header not found in response" diff --git a/docs/typed-rest-client.md b/docs/typed-rest-client.md index 7677650..72d2d57 100644 --- a/docs/typed-rest-client.md +++ b/docs/typed-rest-client.md @@ -24,9 +24,7 @@ const client = createProxyRestClient({ }); const response = await client.get('https://httpbin.org/ip'); -// RestClient API: response has statusCode, result, etc. - -console.log(client.proxyAgent.lastProxyHeaders?.get('x-proxymesh-ip')); +console.log(response.proxyHeaders?.get('x-proxymesh-ip')); ``` ## API Reference @@ -45,7 +43,7 @@ console.log(client.proxyAgent.lastProxyHeaders?.get('x-proxymesh-ip')); | `options.proxyHeaders` | `Object` | CONNECT headers | | `options.onProxyConnect` | `Function` | CONNECT callback | -**Returns:** A `RestClient` instance with an extra `proxyAgent` property (`ProxyHeadersAgent`) for inspecting the last CONNECT response. +**Returns:** A `RestClient` instance with an extra `proxyAgent` property (`ProxyHeadersAgent`). Rest responses include `proxyHeaders` from that request's CONNECT. **Example:** @@ -66,7 +64,7 @@ console.log(client.proxyAgent.lastProxyHeaders); ## Accessing Proxy Headers -Use `client.proxyAgent.lastProxyHeaders` after a request (a `Map`). The typed-rest-client response objects do not merge proxy headers into application response headers the way axios does. +Prefer `response.proxyHeaders.get('header-name')` on the RestClient result. `client.proxyAgent.lastProxyHeaders` is a last-write-wins snapshot and is not safe if the client is used concurrently. CONNECT headers are not merged into origin response headers. ## Factory diff --git a/index.js b/index.js index be9f625..7b83e88 100644 --- a/index.js +++ b/index.js @@ -10,3 +10,8 @@ export { ProxyHeadersAgent, ConnectError } from './lib/core/proxy-headers-agent.js'; export { parseProxyUrl, parseTargetUrl, buildConnectRequest, validateHeaderName, validateHeaderValue } from './lib/core/utils.js'; export { parseConnectResponse, hasCompleteHeaders } from './lib/core/connect-parser.js'; +export { + attachProxyHeaders, + getProxyHeaders, + getProxyHeadersFromSocket, +} from './lib/core/proxy-headers-store.js'; diff --git a/jsr.json b/jsr.json index ddc35a3..2f61b7d 100644 --- a/jsr.json +++ b/jsr.json @@ -1,6 +1,6 @@ { "name": "@proxymesh/javascript-proxy-headers", - "version": "0.2.4", + "version": "0.3.0", "license": "MIT", "exports": "./mod.ts" } diff --git a/lib/axios-proxy.js b/lib/axios-proxy.js index d3fefb3..14a0887 100644 --- a/lib/axios-proxy.js +++ b/lib/axios-proxy.js @@ -6,6 +6,7 @@ */ import { ProxyHeadersAgent } from './core/proxy-headers-agent.js'; +import { getProxyHeaders } from './core/proxy-headers-store.js'; /** * Create an axios instance with proxy header support. @@ -24,7 +25,7 @@ import { ProxyHeadersAgent } from './core/proxy-headers-agent.js'; * }); * * const response = await client.get('https://httpbin.org/ip'); - * console.log(response.headers['x-proxymesh-ip']); + * console.log(response.proxyHeaders.get('x-proxymesh-ip')); */ export async function createProxyAxios(options) { const { proxy, proxyHeaders = {}, onProxyConnect, proxyTlsOptions, axiosOptions = {} } = options; @@ -50,13 +51,7 @@ export async function createProxyAxios(options) { }); instance.interceptors.response.use((response) => { - if (agent.lastProxyHeaders) { - for (const [key, value] of agent.lastProxyHeaders) { - if (!response.headers[key]) { - response.headers[key] = value; - } - } - } + response.proxyHeaders = getProxyHeaders(response) || new Map(); return response; }); diff --git a/lib/core/proxy-headers-agent.js b/lib/core/proxy-headers-agent.js index 126092b..6a6ddd2 100644 --- a/lib/core/proxy-headers-agent.js +++ b/lib/core/proxy-headers-agent.js @@ -9,6 +9,7 @@ import { Agent } from 'node:https'; import tls from 'node:tls'; import { parseProxyUrl, buildConnectRequest, createProxySocket, proxyReadyEvent } from './utils.js'; import { parseConnectResponse, hasCompleteHeaders, ConnectError } from './connect-parser.js'; +import { attachProxyHeaders } from './proxy-headers-store.js'; export class ProxyHeadersAgent extends Agent { /** @@ -122,6 +123,9 @@ export class ProxyHeadersAgent extends Agent { cleanup(); + attachProxyHeaders(proxySocket, response.headers); + // Last-write-wins snapshot for sequential callers. Concurrent + // requests must read per-socket headers via getProxyHeaders(). this.lastProxyHeaders = response.headers; if (this.onProxyConnect) { @@ -151,6 +155,7 @@ export class ProxyHeadersAgent extends Agent { servername: targetHost, ...this.tlsOptions, }); + attachProxyHeaders(tlsSocket, response.headers); tlsSocket.on('error', (err) => { callback(err); diff --git a/lib/core/proxy-headers-store.js b/lib/core/proxy-headers-store.js new file mode 100644 index 0000000..716b45f --- /dev/null +++ b/lib/core/proxy-headers-store.js @@ -0,0 +1,109 @@ +/** + * Per-connection CONNECT header storage. + * + * Headers are attached to the tunnel/TLS socket so concurrent requests on a + * shared agent cannot overwrite each other. AsyncLocalStorage is a fallback + * for fetch wrappers that do not expose the underlying socket. + */ + +import { AsyncLocalStorage } from 'node:async_hooks'; + +export const kProxyHeaders = Symbol.for('javascript-proxy-headers.proxyHeaders'); + +const proxyHeadersStorage = new AsyncLocalStorage(); + +/** + * Record CONNECT response headers on a socket and any active ALS context. + * @param {import('node:net').Socket|null|undefined} socket + * @param {Map|null|undefined} headers + */ +export function attachProxyHeaders(socket, headers) { + if (headers && socket) { + socket[kProxyHeaders] = headers; + } + const store = proxyHeadersStorage.getStore(); + if (store && headers) { + store.headers = headers; + } +} + +/** + * Walk a socket (and TLS parent chain) for attached CONNECT headers. + * @param {object|null|undefined} socket + * @returns {Map|undefined} + */ +export function getProxyHeadersFromSocket(socket) { + const seen = new Set(); + let current = socket; + while (current && !seen.has(current)) { + seen.add(current); + if (current[kProxyHeaders] instanceof Map) { + return current[kProxyHeaders]; + } + current = current._parent || current.socket; + } + return undefined; +} + +/** + * Resolve CONNECT headers from a socket, IncomingMessage, or HTTP client response. + * @param {object|null|undefined} source + * @returns {Map|undefined} + */ +export function getProxyHeaders(source) { + if (!source) { + const store = proxyHeadersStorage.getStore(); + return store?.headers; + } + if (source instanceof Map) { + return source; + } + if (source[kProxyHeaders] instanceof Map) { + return source[kProxyHeaders]; + } + + const candidates = [ + source, + source.socket, + source.connection, + source.request, + source.request?.socket, + source.request?.connection, + source.request?.res, + source.request?.res?.socket, + source.req, + source.req?.socket, + source.req?.connection, + source.res, + source.res?.socket, + source.message, + source.message?.socket, + ]; + + for (const candidate of candidates) { + const found = getProxyHeadersFromSocket(candidate); + if (found) { + return found; + } + } + + return proxyHeadersStorage.getStore()?.headers; +} + +/** + * Run `fn` with an ALS store so CONNECT headers can be recovered without a socket. + * @template T + * @param {() => T} fn + * @returns {T} + */ +export function runWithProxyHeadersContext(fn) { + return proxyHeadersStorage.run({ headers: null }, fn); +} + +/** + * CONNECT headers captured in the current ALS store, if any. + * @returns {Map|null} + */ +export function takeProxyHeadersContext() { + return proxyHeadersStorage.getStore()?.headers ?? null; +} diff --git a/lib/got-proxy.js b/lib/got-proxy.js index e733d52..4dbf53c 100644 --- a/lib/got-proxy.js +++ b/lib/got-proxy.js @@ -6,6 +6,7 @@ */ import { ProxyHeadersAgent } from './core/proxy-headers-agent.js'; +import { getProxyHeaders } from './core/proxy-headers-store.js'; /** * Create a got instance with proxy header support. @@ -24,7 +25,7 @@ import { ProxyHeadersAgent } from './core/proxy-headers-agent.js'; * }); * * const response = await client('https://httpbin.org/ip'); - * console.log(response.headers['x-proxymesh-ip']); + * console.log(response.proxyHeaders.get('x-proxymesh-ip')); */ export async function createProxyGot(options) { const { proxy, proxyHeaders = {}, onProxyConnect, proxyTlsOptions, gotOptions = {} } = options; @@ -50,14 +51,7 @@ export async function createProxyGot(options) { hooks: { afterResponse: [ (response) => { - if (agent.lastProxyHeaders) { - response.proxyHeaders = agent.lastProxyHeaders; - for (const [key, value] of agent.lastProxyHeaders) { - if (!response.headers[key]) { - response.headers[key] = value; - } - } - } + response.proxyHeaders = getProxyHeaders(response) || new Map(); return response; }, ...(gotOptions.hooks?.afterResponse || []), diff --git a/lib/make-fetch-happen-proxy.js b/lib/make-fetch-happen-proxy.js index 4fe8bcc..e0b6052 100644 --- a/lib/make-fetch-happen-proxy.js +++ b/lib/make-fetch-happen-proxy.js @@ -7,10 +7,17 @@ import makeFetchHappen from 'make-fetch-happen'; import { ProxyHeadersAgent } from './core/proxy-headers-agent.js'; import { ProxyResponse } from './core/proxy-response.js'; +import { runWithProxyHeadersContext, takeProxyHeadersContext } from './core/proxy-headers-store.js'; function wrapFetchWithProxyResponse(fetchImpl, agent) { const wrapped = (url, opts = {}) => - fetchImpl(url, opts).then((res) => new ProxyResponse(res, agent.lastProxyHeaders)); + runWithProxyHeadersContext(async () => { + const res = await fetchImpl(url, opts); + return new ProxyResponse( + res, + takeProxyHeadersContext() || agent.lastProxyHeaders, + ); + }); wrapped.defaults = (defaultUrl, defaultOptions = {}) => { const inner = fetchImpl.defaults(defaultUrl, defaultOptions); diff --git a/lib/needle-proxy.js b/lib/needle-proxy.js index e97f891..ae4d2c9 100644 --- a/lib/needle-proxy.js +++ b/lib/needle-proxy.js @@ -4,18 +4,10 @@ import { createRequire } from 'module'; import { ProxyHeadersAgent } from './core/proxy-headers-agent.js'; +import { getProxyHeaders } from './core/proxy-headers-store.js'; const require = createRequire(import.meta.url); -function mergeProxyHeadersIntoResponse(res, map) { - if (!res || !map) return; - for (const [key, value] of map) { - if (res.headers[key] == null) { - res.headers[key] = value; - } - } -} - /** * Perform a GET with proxy headers (promise-based). * @@ -56,7 +48,7 @@ export async function proxyNeedleGet(url, options = {}) { reject(err); return; } - mergeProxyHeadersIntoResponse(res, agent.lastProxyHeaders); + res.proxyHeaders = getProxyHeaders(res) || new Map(); res.proxyAgent = agent; resolve(res); }, @@ -103,7 +95,7 @@ export function createProxyNeedle(options) { reject(err); return; } - mergeProxyHeadersIntoResponse(res, agent.lastProxyHeaders); + res.proxyHeaders = getProxyHeaders(res) || new Map(); res.proxyAgent = agent; resolve(res); }, diff --git a/lib/node-fetch-proxy.js b/lib/node-fetch-proxy.js index 6547e2e..bdce0a3 100644 --- a/lib/node-fetch-proxy.js +++ b/lib/node-fetch-proxy.js @@ -7,6 +7,7 @@ import { ProxyHeadersAgent } from './core/proxy-headers-agent.js'; import { ProxyResponse } from './core/proxy-response.js'; +import { runWithProxyHeadersContext, takeProxyHeadersContext } from './core/proxy-headers-store.js'; /** * Fetch with proxy header support. @@ -62,9 +63,13 @@ export async function proxyFetch(url, options = {}) { }; } - const response = await fetch(requestUrl, init); - - return new ProxyResponse(response, agent.lastProxyHeaders); + return runWithProxyHeadersContext(async () => { + const response = await fetch(requestUrl, init); + return new ProxyResponse( + response, + takeProxyHeadersContext() || agent.lastProxyHeaders, + ); + }); } /** diff --git a/lib/superagent-proxy.js b/lib/superagent-proxy.js index 6b58397..b4f6428 100644 --- a/lib/superagent-proxy.js +++ b/lib/superagent-proxy.js @@ -6,6 +6,7 @@ */ import { ProxyHeadersAgent } from './core/proxy-headers-agent.js'; +import { getProxyHeaders } from './core/proxy-headers-store.js'; /** * Create a superagent plugin for proxy header support. @@ -27,7 +28,7 @@ import { ProxyHeadersAgent } from './core/proxy-headers-agent.js'; * proxyHeaders: { 'X-ProxyMesh-Country': 'US' } * })); * - * console.log(response.headers['x-proxymesh-ip']); + * console.log(response.proxyHeaders.get('x-proxymesh-ip')); */ export function proxyPlugin(options) { const { proxy, proxyHeaders = {}, onProxyConnect, proxyTlsOptions } = options; @@ -48,13 +49,8 @@ export function proxyPlugin(options) { const originalEnd = request.end.bind(request); request.end = function (callback) { return originalEnd((err, res) => { - if (!err && res && agent.lastProxyHeaders) { - res.proxyHeaders = agent.lastProxyHeaders; - for (const [key, value] of agent.lastProxyHeaders) { - if (!res.headers[key]) { - res.headers[key] = value; - } - } + if (res) { + res.proxyHeaders = getProxyHeaders(res) || new Map(); } if (callback) { callback(err, res); diff --git a/lib/typed-rest-client-proxy.js b/lib/typed-rest-client-proxy.js index 92019d2..941549f 100644 --- a/lib/typed-rest-client-proxy.js +++ b/lib/typed-rest-client-proxy.js @@ -6,6 +6,7 @@ import { createRequire } from 'module'; import { ProxyHeadersAgent } from './core/proxy-headers-agent.js'; +import { getProxyHeaders } from './core/proxy-headers-store.js'; const require = createRequire(import.meta.url); @@ -39,6 +40,20 @@ function createProxyHeadersHttpClientClass(HttpClient) { } return super._getAgent(parsedUrl); } + + request(verb, requestUrl, data, headers) { + const result = super.request(verb, requestUrl, data, headers); + const attach = (res) => { + if (res) { + res.proxyHeaders = getProxyHeaders(res.message) || new Map(); + } + return res; + }; + if (result && typeof result.then === 'function') { + return result.then(attach); + } + return attach(result); + } }; } @@ -67,6 +82,20 @@ function createProxyHeadersRestClientClass(RestClient, ProxyHeadersHttpClient) { ); this.proxyAgent = this.client.proxyAgent; } + + _processResponse(res, options) { + const processed = super._processResponse(res, options); + const attach = (out) => { + if (out) { + out.proxyHeaders = res?.proxyHeaders || getProxyHeaders(res?.message) || new Map(); + } + return out; + }; + if (processed && typeof processed.then === 'function') { + return processed.then(attach); + } + return attach(processed); + } }; } diff --git a/lib/undici-proxy.js b/lib/undici-proxy.js index d48eca5..45434ef 100644 --- a/lib/undici-proxy.js +++ b/lib/undici-proxy.js @@ -7,6 +7,7 @@ import { parseProxyUrl, buildConnectRequest, createProxySocket, proxyReadyEvent } from './core/utils.js'; import { parseConnectResponse, hasCompleteHeaders, ConnectError } from './core/connect-parser.js'; +import { attachProxyHeaders } from './core/proxy-headers-store.js'; /** * Create a tunnel through the proxy with custom headers. @@ -86,6 +87,7 @@ async function createProxyTunnel(options) { } cleanup(); + attachProxyHeaders(socket, response.headers); if (response.statusCode !== 200) { socket.destroy(); diff --git a/package.json b/package.json index 4c68b17..f379b4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "javascript-proxy-headers", - "version": "0.2.4", + "version": "0.3.0", "description": "Extensions for JavaScript HTTP libraries to support sending and receiving custom proxy headers during HTTPS CONNECT tunneling", "type": "module", "main": "index.js", diff --git a/test/test_connect_security.js b/test/test_connect_security.js index 4293232..7192578 100644 --- a/test/test_connect_security.js +++ b/test/test_connect_security.js @@ -20,6 +20,8 @@ import { proxyReadyEvent, } from '../lib/core/utils.js'; import { ProxyHeadersAgent } from '../lib/core/proxy-headers-agent.js'; +import { createProxyAxios } from '../lib/axios-proxy.js'; +import { getProxyHeaders } from '../lib/core/proxy-headers-store.js'; test('buildConnectRequest rejects CRLF in target host', () => { assert.throws( @@ -233,3 +235,148 @@ function makeSelfSignedCert() { cleanup: () => rmSync(dir, { recursive: true, force: true }), }; } + +function createMitmConnectProxy({ cert, key, headerFactory, delayMs = 0 }) { + let connectCount = 0; + const server = net.createServer((sock) => { + let buf = Buffer.alloc(0); + let handedOff = false; + sock.on('data', (d) => { + if (handedOff) return; + buf = Buffer.concat([buf, d]); + if (buf.indexOf('\r\n\r\n') === -1) return; + handedOff = true; + connectCount += 1; + const id = connectCount; + sock.write( + 'HTTP/1.1 200 Connection Established\r\n' + + headerFactory({ id }) + + '\r\n', + ); + const tlsSock = new tls.TLSSocket(sock, { isServer: true, cert, key }); + tlsSock.on('secure', async () => { + if (delayMs) await delay(delayMs); + let httpBuf = Buffer.alloc(0); + const tryRespond = () => { + if (!httpBuf.includes('\r\n\r\n')) return; + const reqLine = httpBuf.toString('utf8').split('\r\n')[0]; + const body = JSON.stringify({ ok: true, id, reqLine }); + tlsSock.write( + 'HTTP/1.1 200 OK\r\n' + + 'Content-Type: application/json\r\n' + + `Content-Length: ${Buffer.byteLength(body)}\r\n` + + 'Connection: close\r\n' + + '\r\n' + + body, + ); + tlsSock.end(); + }; + tlsSock.on('data', (chunk) => { + httpBuf = Buffer.concat([httpBuf, chunk]); + tryRespond(); + }); + }); + }); + sock.on('error', () => {}); + }); + return server; +} + +test('axios does not merge CONNECT Set-Cookie into origin response.headers', async () => { + const { cert, key, cleanup } = makeSelfSignedCert(); + const proxy = createMitmConnectProxy({ + cert, + key, + headerFactory: () => + 'X-ProxyMesh-IP: 203.0.113.9\r\nSet-Cookie: session=attacker-injected\r\nLocation: https://evil.example/\r\n', + }); + await listen(proxy); + try { + const { port } = proxy.address(); + const client = await createProxyAxios({ + proxy: `http://127.0.0.1:${port}`, + }); + client.proxyAgent.tlsOptions = { rejectUnauthorized: false }; + const response = await client.get('https://127.0.0.1/'); + assert.equal(response.proxyHeaders.get('x-proxymesh-ip'), '203.0.113.9'); + assert.equal(response.proxyHeaders.get('set-cookie'), 'session=attacker-injected'); + assert.equal(response.headers['set-cookie'], undefined); + assert.equal(response.headers.location, undefined); + assert.match(String(response.headers['content-type']), /application\/json/); + } finally { + proxy.close(); + cleanup(); + } +}); + +test('concurrent axios requests keep per-response CONNECT headers', async () => { + const { cert, key, cleanup } = makeSelfSignedCert(); + const proxy = createMitmConnectProxy({ + cert, + key, + delayMs: 200, + headerFactory: ({ id }) => `X-ProxyMesh-IP: 198.51.100.${id}\r\nX-Race-Id: ${id}\r\n`, + }); + await listen(proxy); + try { + const { port } = proxy.address(); + const client = await createProxyAxios({ + proxy: `http://127.0.0.1:${port}`, + }); + client.proxyAgent.tlsOptions = { rejectUnauthorized: false }; + client.proxyAgent.maxSockets = 10; + const results = await Promise.all( + ['/a', '/b', '/c', '/d'].map(async (path) => { + const response = await client.get(`https://127.0.0.1${path}`); + return { + body: response.data, + raceId: response.proxyHeaders.get('x-race-id'), + ip: response.proxyHeaders.get('x-proxymesh-ip'), + }; + }), + ); + for (const row of results) { + assert.equal(row.ip, `198.51.100.${row.body.id}`); + assert.equal(row.raceId, String(row.body.id)); + } + } finally { + proxy.close(); + cleanup(); + } +}); + +test('getProxyHeaders reads CONNECT headers from the TLS socket', async () => { + const { cert, key, cleanup } = makeSelfSignedCert(); + const proxy = createMitmConnectProxy({ + cert, + key, + headerFactory: () => 'X-ProxyMesh-IP: 192.0.2.8\r\n', + }); + await listen(proxy); + try { + const { port } = proxy.address(); + const agent = new ProxyHeadersAgent(`http://127.0.0.1:${port}`, { + tlsOptions: { rejectUnauthorized: false }, + }); + const incoming = await new Promise((resolve, reject) => { + const req = https.request({ + hostname: '127.0.0.1', + port: 443, + path: '/', + method: 'GET', + agent, + rejectUnauthorized: false, + }, (res) => { + res.resume(); + res.on('end', () => resolve(res)); + }); + req.on('error', reject); + req.end(); + }); + const headers = getProxyHeaders(incoming); + assert.equal(headers.get('x-proxymesh-ip'), '192.0.2.8'); + } finally { + proxy.close(); + cleanup(); + } +}); diff --git a/test/test_proxy_headers.js b/test/test_proxy_headers.js index 90324a9..8c67bad 100644 --- a/test/test_proxy_headers.js +++ b/test/test_proxy_headers.js @@ -209,7 +209,7 @@ const AVAILABLE_TESTS = { const response = await client.get(config.testUrl, { validateStatus: () => true, }); - const headerValue = checkHeader(response.headers, config.proxyHeader); + const headerValue = checkHeader(response.proxyHeaders, config.proxyHeader); const sentErr = validateSentHeaderValue(config, headerValue); if (sentErr) return new TestResult('axios', false, null, sentErr); @@ -259,7 +259,7 @@ const AVAILABLE_TESTS = { }); const response = await client(config.testUrl); - const headerValue = checkHeader(response.headers, config.proxyHeader); + const headerValue = checkHeader(response.proxyHeaders, config.proxyHeader); const sentErr = validateSentHeaderValue(config, headerValue); if (sentErr) return new TestResult('got', false, null, sentErr); @@ -308,7 +308,7 @@ const AVAILABLE_TESTS = { }); const response = await client.get(config.testUrl).ok(() => true); - const headerValue = checkHeader(response.headers, config.proxyHeader); + const headerValue = checkHeader(response.proxyHeaders, config.proxyHeader); const sentErr = validateSentHeaderValue(config, headerValue); if (sentErr) return new TestResult('superagent', false, null, sentErr); @@ -415,7 +415,7 @@ const AVAILABLE_TESTS = { proxyHeaders: config.proxyHeadersToSend, }); - const headerValue = checkHeader(res.headers, config.proxyHeader); + const headerValue = checkHeader(res.proxyHeaders, config.proxyHeader); const sentErr = validateSentHeaderValue(config, headerValue); if (sentErr) return new TestResult('needle', false, null, sentErr); @@ -442,7 +442,10 @@ const AVAILABLE_TESTS = { }); const result = await client.get(config.testUrl); - const headerValue = checkHeader(client.proxyAgent.lastProxyHeaders, config.proxyHeader); + const headerValue = checkHeader( + result.proxyHeaders || client.proxyAgent.lastProxyHeaders, + config.proxyHeader, + ); const sentErr = validateSentHeaderValue(config, headerValue); if (sentErr) return new TestResult('typed-rest-client', false, null, sentErr); diff --git a/test/test_proxy_headers.ts b/test/test_proxy_headers.ts index 56c825e..b437f00 100644 --- a/test/test_proxy_headers.ts +++ b/test/test_proxy_headers.ts @@ -164,7 +164,7 @@ const AVAILABLE_TESTS: Record = { }); const response = await client.get(config.testUrl, { validateStatus: () => true }); - const headerValue = checkHeader(response.headers as Record, config.proxyHeader); + const headerValue = checkHeader((response as { proxyHeaders?: Map }).proxyHeaders, config.proxyHeader); const sentErr = validateSentHeaderValue(config, headerValue); if (sentErr) return new TestResult("axios", false, null, sentErr); @@ -209,7 +209,7 @@ const AVAILABLE_TESTS: Record = { }); const response = await client(config.testUrl); - const headerValue = checkHeader(response.headers as Record, config.proxyHeader); + const headerValue = checkHeader((response as { proxyHeaders?: Map }).proxyHeaders, config.proxyHeader); const sentErr = validateSentHeaderValue(config, headerValue); if (sentErr) return new TestResult("got", false, null, sentErr); @@ -253,7 +253,7 @@ const AVAILABLE_TESTS: Record = { }); const response = await client.get(config.testUrl).ok(() => true); - const headerValue = checkHeader(response.headers as Record, config.proxyHeader); + const headerValue = checkHeader((response as { proxyHeaders?: Map }).proxyHeaders, config.proxyHeader); const sentErr = validateSentHeaderValue(config, headerValue); if (sentErr) return new TestResult("superagent", false, null, sentErr); @@ -355,7 +355,7 @@ const AVAILABLE_TESTS: Record = { proxy: config.proxyUrl!, proxyHeaders: config.proxyHeadersToSend, }); - const headerValue = checkHeader(res.headers as Record, config.proxyHeader); + const headerValue = checkHeader((res as { proxyHeaders?: Map }).proxyHeaders, config.proxyHeader); const sentErr = validateSentHeaderValue(config, headerValue); if (sentErr) return new TestResult("needle", false, null, sentErr); if (headerValue) return new TestResult("needle", true, headerValue, null, res.statusCode); @@ -381,7 +381,10 @@ const AVAILABLE_TESTS: Record = { proxyHeaders: config.proxyHeadersToSend, }); const result = await client.get(config.testUrl); - const headerValue = checkHeader(client.proxyAgent.lastProxyHeaders as Map, config.proxyHeader); + const headerValue = checkHeader( + (result.proxyHeaders || client.proxyAgent.lastProxyHeaders) as Map, + config.proxyHeader, + ); const sentErr = validateSentHeaderValue(config, headerValue); if (sentErr) return new TestResult("typed-rest-client", false, null, sentErr); if (headerValue) return new TestResult("typed-rest-client", true, headerValue, null, result.statusCode); diff --git a/test/types.test.ts b/test/types.test.ts index 5d53e81..33c624c 100644 --- a/test/types.test.ts +++ b/test/types.test.ts @@ -3,6 +3,7 @@ import { parseProxyUrl, parseTargetUrl, buildConnectRequest, + getProxyHeaders, } from "javascript-proxy-headers"; import { createProxyAxios, type ProxyAxiosInstance, type CreateProxyAxiosOptions } from "javascript-proxy-headers/axios"; import { createProxyFetch, type ProxyResponse } from "javascript-proxy-headers/node-fetch"; @@ -39,6 +40,7 @@ async function typecheck() { const axiosPromise: Promise = createProxyAxios(axiosOptions); const axiosInstance = await axiosPromise; axiosInstance.proxyAgent.lastProxyHeaders; + void getProxyHeaders; const proxyFetch = createProxyFetch({ proxy: "http://proxy.example.com:8080", diff --git a/types/axios.d.ts b/types/axios.d.ts index e822550..bc6a163 100644 --- a/types/axios.d.ts +++ b/types/axios.d.ts @@ -14,6 +14,11 @@ export interface CreateProxyAxiosOptions { axiosOptions?: object; } +export interface AxiosResponseWithProxyHeaders extends AxiosResponse { + /** CONNECT response headers for this request (not merged into origin headers) */ + proxyHeaders: Map; +} + export interface ProxyAxiosInstance extends AxiosInstance { proxyAgent: ProxyHeadersAgent; } diff --git a/types/got.d.ts b/types/got.d.ts index 59fd6ef..7110f99 100644 --- a/types/got.d.ts +++ b/types/got.d.ts @@ -14,6 +14,11 @@ export interface CreateProxyGotOptions { gotOptions?: object; } +export interface GotResponseWithProxyHeaders extends GotResponse { + /** CONNECT response headers for this request (not merged into origin headers) */ + proxyHeaders: Map; +} + export interface ProxyGotInstance extends Got { proxyAgent: ProxyHeadersAgent; } diff --git a/types/index.d.ts b/types/index.d.ts index 4ac0bd0..26d15c3 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -36,7 +36,11 @@ export class ProxyHeadersAgent extends Agent { readonly proxyTlsOptions: object; /** Headers to send to proxy */ readonly proxyHeaders: Record; - /** Headers from last CONNECT response */ + /** + * Headers from the most recently completed CONNECT. + * Last-write-wins if the agent is used concurrently — use + * `getProxyHeaders()` or adapter `response.proxyHeaders` instead. + */ lastProxyHeaders: Map | null; } @@ -87,3 +91,13 @@ export function buildConnectRequest( ): string; export function parseConnectResponse(data: Buffer | string): ConnectResponse | null; export function hasCompleteHeaders(buffer: Buffer): boolean; +export function attachProxyHeaders( + socket: object | null | undefined, + headers: Map | null | undefined, +): void; +export function getProxyHeadersFromSocket( + socket: object | null | undefined, +): Map | undefined; +export function getProxyHeaders( + source?: object | null, +): Map | undefined; diff --git a/types/needle.d.ts b/types/needle.d.ts index 2df484e..ff77fd0 100644 --- a/types/needle.d.ts +++ b/types/needle.d.ts @@ -6,7 +6,13 @@ export interface ProxyNeedleOptions { needleOptions?: Record; } -export function proxyNeedleGet(url: string, options: ProxyNeedleOptions): Promise; +export interface NeedleResponseWithProxyHeaders { + proxyHeaders: Map; + proxyAgent: import('./index.js').ProxyHeadersAgent; + [key: string]: unknown; +} + +export function proxyNeedleGet(url: string, options: ProxyNeedleOptions): Promise; export interface CreateProxyNeedleOptions { proxy: string; diff --git a/types/superagent.d.ts b/types/superagent.d.ts index d4c9ed4..7211500 100644 --- a/types/superagent.d.ts +++ b/types/superagent.d.ts @@ -13,8 +13,8 @@ export interface ProxyPluginOptions { } export interface ProxyResponse extends Response { - /** Headers from proxy CONNECT response */ - proxyHeaders?: Map; + /** CONNECT response headers for this request (not merged into origin headers) */ + proxyHeaders: Map; } export function proxyPlugin(