Skip to content

Commit 4ee0ac7

Browse files
authored
Merge pull request #11 from proxymesh/feature/tier-a-proxy-extensions
Release v0.2.0: Tier A proxy extensions
2 parents 899d48c + efde9ad commit 4ee0ac7

19 files changed

Lines changed: 4402 additions & 70 deletions

README.md

Lines changed: 104 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,30 @@ This library solves both problems for popular JavaScript HTTP libraries.
1414

1515
## Supported Libraries
1616

17-
| Library | Module | Use Case |
18-
|---------|--------|----------|
19-
| [axios](https://axios-http.com/) | `axios-proxy` | Most popular HTTP client |
20-
| [node-fetch](https://github.com/node-fetch/node-fetch) | `node-fetch-proxy` | Fetch API for Node.js |
21-
| [got](https://github.com/sindresorhus/got) | `got-proxy` | Human-friendly HTTP client |
22-
| [undici](https://undici.nodejs.org/) | `undici-proxy` | Fast HTTP client (Node.js core) |
23-
| [superagent](https://github.com/ladjs/superagent) | `superagent-proxy` | Flexible HTTP client |
17+
| Library | Subpath export | Notes |
18+
|---------|----------------|--------|
19+
| [axios](https://axios-http.com/) | `javascript-proxy-headers/axios` | Widely used client |
20+
| [node-fetch](https://github.com/node-fetch/node-fetch) | `javascript-proxy-headers/node-fetch` | Fetch API on Node |
21+
| [got](https://github.com/sindresorhus/got) | `javascript-proxy-headers/got` | Ergonomic API |
22+
| [undici](https://undici.nodejs.org/) | `javascript-proxy-headers/undici` | Node’s fast HTTP stack |
23+
| [superagent](https://github.com/ladjs/superagent) | `javascript-proxy-headers/superagent` | Chaining API |
24+
| [ky](https://github.com/sindresorhus/ky) | `javascript-proxy-headers/ky` | Tiny fetch wrapper |
25+
| [wretch](https://github.com/elbywan/wretch) | `javascript-proxy-headers/wretch` | Fetch wrapper (sets wretch’s global fetch polyfill) |
26+
| [make-fetch-happen](https://github.com/npm/make-fetch-happen) | `javascript-proxy-headers/make-fetch-happen` | npm-style fetch (cache, retries, proxy) |
27+
| [needle](https://github.com/tomas/needle) | `javascript-proxy-headers/needle` | Lean HTTP client |
28+
| [typed-rest-client](https://github.com/microsoft/typed-rest-client) | `javascript-proxy-headers/typed-rest-client` | Azure / DevOps–style REST client |
29+
30+
**urllib** is not integrated yet: it expects an [undici](https://undici.nodejs.org/) `Dispatcher`, not a Node `Agent`. See `notes/urllib-integration-deferred.md` in this repo for a possible approach.
2431

2532
## Installation
2633

2734
```bash
2835
npm install javascript-proxy-headers
2936
```
3037

31-
Then install the HTTP library you want to use (e.g., `npm install axios`).
38+
Then install the HTTP client(s) you use (for example `axios`, `got`, `ky`, `wretch`, `make-fetch-happen`, `needle`, or `typed-rest-client`). Each is an optional peer dependency.
3239

33-
> **Note:** This package has no dependencies by default - install only what you need.
40+
> **Note:** This package has no runtime dependencies by defaultinstall only the adapters you need.
3441
3542
## Quick Start
3643

@@ -94,6 +101,85 @@ const { statusCode, headers, body, proxyHeaders } = await request(
94101
console.log(proxyHeaders.get('x-proxymesh-ip'));
95102
```
96103

104+
### ky
105+
106+
Uses a custom `fetch` built from node-fetch + `ProxyHeadersAgent` (`ky.create({ fetch })`).
107+
108+
```javascript
109+
import { createProxyKy } from 'javascript-proxy-headers/ky';
110+
111+
const api = await createProxyKy({
112+
proxy: 'http://user:pass@proxy.example.com:8080',
113+
proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
114+
});
115+
116+
const response = await api('https://httpbin.org/ip');
117+
console.log(response.proxyHeaders.get('x-proxymesh-ip'));
118+
```
119+
120+
### wretch
121+
122+
Registers the same custom `fetch` as wretch’s fetch polyfill. Use the normal wretch chain (for example `.get().res()`).
123+
124+
```javascript
125+
import { createProxyWretch } from 'javascript-proxy-headers/wretch';
126+
127+
const wretch = await createProxyWretch({
128+
proxy: 'http://user:pass@proxy.example.com:8080',
129+
proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
130+
});
131+
132+
const response = await wretch('https://httpbin.org/ip').get().res();
133+
console.log(response.proxyHeaders.get('x-proxymesh-ip'));
134+
```
135+
136+
### make-fetch-happen
137+
138+
Passes a `ProxyHeadersAgent` as `agent`; `@npmcli/agent` uses it as-is when set.
139+
140+
```javascript
141+
import { createProxyMakeFetchHappen } from 'javascript-proxy-headers/make-fetch-happen';
142+
143+
const fetch = createProxyMakeFetchHappen({
144+
proxy: 'http://user:pass@proxy.example.com:8080',
145+
proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
146+
});
147+
148+
const response = await fetch('https://httpbin.org/ip');
149+
console.log(response.proxyHeaders.get('x-proxymesh-ip'));
150+
```
151+
152+
### needle
153+
154+
```javascript
155+
import { proxyNeedleGet } from 'javascript-proxy-headers/needle';
156+
157+
const res = await proxyNeedleGet('https://httpbin.org/ip', {
158+
proxy: 'http://user:pass@proxy.example.com:8080',
159+
proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
160+
});
161+
162+
// CONNECT response headers merged onto res.headers where missing
163+
console.log(res.headers['x-proxymesh-ip']);
164+
```
165+
166+
### typed-rest-client
167+
168+
Uses a subclass of `HttpClient` that routes HTTPS through `ProxyHeadersAgent` (no `tunnel` agent).
169+
170+
```javascript
171+
import { createProxyRestClient } from 'javascript-proxy-headers/typed-rest-client';
172+
173+
const client = createProxyRestClient({
174+
userAgent: 'my-app',
175+
proxy: 'http://user:pass@proxy.example.com:8080',
176+
proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
177+
});
178+
179+
await client.get('https://httpbin.org/ip');
180+
console.log(client.proxyAgent.lastProxyHeaders?.get('x-proxymesh-ip'));
181+
```
182+
97183
### Core Agent (Advanced)
98184
99185
For direct control, use the core `ProxyHeadersAgent`:
@@ -116,19 +202,22 @@ https.get('https://httpbin.org/ip', { agent }, (res) => {
116202
117203
## Testing
118204
119-
A test harness is included to verify proxy header functionality:
205+
Integration tests need a real proxy (set `PROXY_URL` or `HTTPS_PROXY`):
120206
121207
```bash
122-
# Set your proxy
123208
export PROXY_URL='http://user:pass@proxy.example.com:8080'
124209

125-
# Test all modules
126-
npm test
210+
npm test # all adapters (see package.json "test")
211+
node run_tests.js -v # same harness from repo root
212+
npm run test:ts # same checks via tsx + TypeScript harness
213+
npm run test:types # `tsc --noEmit` only (no network)
127214

128-
# Test specific module
129-
npm test axios
215+
# Limit modules
216+
node test/test_proxy_headers.js -v axios ky
130217
```
131218
219+
Verbose (`-v`) prints captured header values. See `test/test_proxy_headers.js --help`.
220+
132221
## Requirements
133222
134223
- Node.js >= 18.0.0

lib/core/proxy-response.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Wraps a fetch Response and exposes CONNECT proxy response headers.
3+
*/
4+
5+
export class ProxyResponse {
6+
/**
7+
* @param {import('node-fetch').Response} response
8+
* @param {Map<string, string>|null|undefined} proxyHeaders
9+
*/
10+
constructor(response, proxyHeaders) {
11+
this._response = response;
12+
this.proxyHeaders = proxyHeaders || new Map();
13+
14+
this.ok = response.ok;
15+
this.status = response.status;
16+
this.statusText = response.statusText;
17+
this.headers = response.headers;
18+
this.url = response.url;
19+
this.redirected = response.redirected;
20+
this.type = response.type;
21+
this.body = response.body;
22+
this.bodyUsed = response.bodyUsed;
23+
}
24+
25+
async text() {
26+
return this._response.text();
27+
}
28+
29+
async json() {
30+
return this._response.json();
31+
}
32+
33+
async blob() {
34+
return this._response.blob();
35+
}
36+
37+
async arrayBuffer() {
38+
return this._response.arrayBuffer();
39+
}
40+
41+
async formData() {
42+
return this._response.formData();
43+
}
44+
45+
clone() {
46+
return new ProxyResponse(this._response.clone(), this.proxyHeaders);
47+
}
48+
}

lib/ky-proxy.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* ky extension for proxy header support.
3+
*
4+
* Uses a custom `fetch` backed by node-fetch + ProxyHeadersAgent.
5+
*/
6+
7+
import { createProxyFetch } from './node-fetch-proxy.js';
8+
9+
/**
10+
* Create a ky instance with proxy header support.
11+
*
12+
* @param {Object} options - Configuration
13+
* @param {string} options.proxy - Proxy URL
14+
* @param {Object} [options.proxyHeaders] - Headers to send on CONNECT
15+
* @param {Function} [options.onProxyConnect] - CONNECT callback
16+
* @param {Object} [options.kyOptions] - Options passed to ky.create()
17+
* @returns {Promise<import('ky').KyInstance>}
18+
*/
19+
export async function createProxyKy(options) {
20+
const { proxy, proxyHeaders = {}, onProxyConnect, kyOptions = {} } = options;
21+
22+
if (!proxy) {
23+
throw new Error('proxy option is required');
24+
}
25+
26+
let ky;
27+
try {
28+
ky = (await import('ky')).default;
29+
} catch {
30+
throw new Error('ky is required. Install it with: npm install ky');
31+
}
32+
33+
const fetch = createProxyFetch({ proxy, proxyHeaders, onProxyConnect });
34+
35+
return ky.create({
36+
...kyOptions,
37+
fetch,
38+
});
39+
}

lib/make-fetch-happen-proxy.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* make-fetch-happen extension for proxy header support.
3+
*
4+
* Passes a ProxyHeadersAgent via opts.agent; @npmcli/agent returns it as-is when set.
5+
*/
6+
7+
import makeFetchHappen from 'make-fetch-happen';
8+
import { ProxyHeadersAgent } from './core/proxy-headers-agent.js';
9+
import { ProxyResponse } from './core/proxy-response.js';
10+
11+
function wrapFetchWithProxyResponse(fetchImpl, agent) {
12+
const wrapped = (url, opts = {}) =>
13+
fetchImpl(url, opts).then((res) => new ProxyResponse(res, agent.lastProxyHeaders));
14+
15+
wrapped.defaults = (defaultUrl, defaultOptions = {}) => {
16+
const inner = fetchImpl.defaults(defaultUrl, defaultOptions);
17+
return wrapFetchWithProxyResponse(inner, agent);
18+
};
19+
20+
wrapped.proxyAgent = agent;
21+
return wrapped;
22+
}
23+
24+
/**
25+
* Create a make-fetch-happen fetch function with proxy header support.
26+
*
27+
* @param {Object} options - Configuration
28+
* @param {string} options.proxy - Proxy URL
29+
* @param {Object} [options.proxyHeaders] - Headers to send on CONNECT
30+
* @param {Function} [options.onProxyConnect] - CONNECT callback
31+
* @param {Object} [options.defaults] - Extra options for make-fetch-happen.defaults()
32+
* @returns {Function} Fetch function with .defaults() and .proxyAgent
33+
*/
34+
export function createProxyMakeFetchHappen(options) {
35+
const { proxy, proxyHeaders = {}, onProxyConnect, ...makeFetchHappenOptions } = options;
36+
37+
if (!proxy) {
38+
throw new Error('proxy option is required');
39+
}
40+
41+
const agent = new ProxyHeadersAgent(proxy, {
42+
proxyHeaders,
43+
onProxyConnect,
44+
});
45+
46+
const inner = makeFetchHappen.defaults({
47+
...makeFetchHappenOptions,
48+
agent,
49+
});
50+
51+
return wrapFetchWithProxyResponse(inner, agent);
52+
}

0 commit comments

Comments
 (0)