Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/javascript-proxy-headers/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Install your HTTP client as a peer dependency (axios, got, node-fetch, etc.).
```javascript
import { createProxyAxios } from 'javascript-proxy-headers/axios';

const client = createProxyAxios({
const client = await createProxyAxios({
proxy: 'http://user:pass@proxy.example.com:8080',
proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
});
Expand All @@ -67,7 +67,7 @@ console.log(response.proxyHeaders.get('x-proxymesh-ip'));
```javascript
import { createProxyGot } from 'javascript-proxy-headers/got';

const client = createProxyGot({
const client = await createProxyGot({
proxy: 'http://user:pass@proxy.example.com:8080',
proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
});
Expand Down
88 changes: 88 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Unit tests, typecheck, and local mock-proxy integration.
# Real-proxy integration stays in proxy_integration_tests.yml (needs PROXY_URL).

name: CI

on:
pull_request:
push:
branches:
- main

permissions:
contents: read
# Required for setup-node npm cache save under restricted GITHUB_TOKEN scopes.
actions: write

jobs:
unit:
name: unit (Node ${{ matrix.node }})
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
node: ["22", "24"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node }}
cache: npm

- name: Install dependencies
run: npm ci

- name: Unit tests
run: npm run test:unit

types:
name: types (Node ${{ matrix.node }})
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
node: ["22", "24"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node }}
cache: npm

- name: Install dependencies
run: npm ci

- name: Typecheck
run: npm run test:types

local-mock-proxy:
name: local mock proxy (Node ${{ matrix.node }})
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
node: ["22", "24"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node }}
cache: npm

- name: Install dependencies
run: npm ci

- name: Adapter tests through a local CONNECT proxy
run: npm run test:local-proxy
59 changes: 59 additions & 0 deletions .github/workflows/latest-clients.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Weekly (and manual) run against the latest published versions of every
# supported HTTP client. Ignores the lockfile for those clients. Does not
# change Dependabot auto-merge.

name: Latest clients

on:
schedule:
# Monday 12:00 UTC, after the weekly Dependabot window.
- cron: "0 12 * * 1"
workflow_dispatch:

permissions:
contents: read
actions: write

jobs:
latest:
name: latest clients (Node ${{ matrix.node }})
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
node: ["22", "24"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node }}
cache: npm

- name: Install lockfile dependencies
run: npm ci

- name: Install latest supported clients (ignore the lockfile)
run: |
set -euo pipefail
npm install --no-save --no-package-lock \
axios@latest \
got@latest \
ky@latest \
make-fetch-happen@latest \
needle@latest \
node-fetch@latest \
superagent@latest \
typed-rest-client@latest \
undici@latest \
wretch@latest
npm ls --depth=0 axios got ky make-fetch-happen needle node-fetch superagent typed-rest-client undici wretch

- name: Unit tests
run: npm run test:unit

- name: Local mock-proxy integration (includes proxy-traversal assertion)
run: npm run test:local-proxy
4 changes: 2 additions & 2 deletions IMPLEMENTATION_PRIORITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ console.log(response.proxyHeaders.get('x-proxymesh-ip'));
```javascript
import { createProxyAxios } from 'javascript-proxy-headers/axios';

const client = createProxyAxios({
const client = await createProxyAxios({
proxy: 'http://proxy:8080',
proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
});
Expand All @@ -151,7 +151,7 @@ console.log(response.headers['x-proxymesh-ip']); // Merged into response
```javascript
import { createProxyGot } from 'javascript-proxy-headers/got';

const client = createProxyGot({
const client = await createProxyGot({
proxy: 'http://proxy:8080',
proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
});
Expand Down
2 changes: 1 addition & 1 deletion LIBRARY_RESEARCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ console.log(response.proxyHeaders.get('X-ProxyMesh-IP'));
```javascript
import { createProxyGot } from 'javascript-proxy-headers/got';

const proxyGot = createProxyGot({
const proxyGot = await createProxyGot({
proxy: 'http://proxy:8080',
proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
});
Expand Down
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[![npm version](https://badge.fury.io/js/javascript-proxy-headers.svg)](https://badge.fury.io/js/javascript-proxy-headers)
# JavaScript Proxy Headers

Extensions for JavaScript HTTP libraries to support **sending and receiving custom proxy headers** during HTTPS CONNECT tunneling.
[![npm version](https://badge.fury.io/js/javascript-proxy-headers.svg)](https://badge.fury.io/js/javascript-proxy-headers)

Send and read custom proxy headers on HTTPS CONNECT for axios, got, node-fetch, undici, ky, and more.

## The Problem

Expand All @@ -28,7 +29,7 @@ This library solves both problems for popular JavaScript HTTP libraries.
| [needle](https://github.com/tomas/needle) | `javascript-proxy-headers/needle` | Lean HTTP client |
| [typed-rest-client](https://github.com/microsoft/typed-rest-client) | `javascript-proxy-headers/typed-rest-client` | Azure / DevOps–style REST client |

**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.
**urllib** is not integrated yet: it expects an [undici](https://undici.nodejs.org/) `Dispatcher`, not a Node `Agent`. See [notes/urllib-integration-deferred.md](https://github.com/proxymesh/javascript-proxy-headers/blob/main/notes/urllib-integration-deferred.md) for a possible approach.

## Installation

Expand Down Expand Up @@ -61,7 +62,7 @@ response.proxyHeaders.get('x-proxymesh-ip')
```javascript
import { createProxyAxios } from 'javascript-proxy-headers/axios';

const client = createProxyAxios({
const client = await createProxyAxios({
proxy: 'http://user:pass@proxy.example.com:8080',
proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
});
Expand Down Expand Up @@ -90,7 +91,7 @@ console.log(response.proxyHeaders.get('x-proxymesh-ip'));
```javascript
import { createProxyGot } from 'javascript-proxy-headers/got';

const client = createProxyGot({
const client = await createProxyGot({
proxy: 'http://user:pass@proxy.example.com:8080',
proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
});
Expand Down Expand Up @@ -234,18 +235,20 @@ Verbose (`-v`) prints captured header values. See `test/test_proxy_headers.js --

## Requirements

- Node.js >= 18.0.0
- Node.js >= 22.19.0 (undici 8, got 16, and ky 2 require current Node 22)
- One or more supported HTTP libraries

## Related Projects
## Other languages

- **[python-proxy-headers](https://github.com/proxymesh/python-proxy-headers)** - Same functionality for Python
- **[proxy-examples](https://github.com/proxymesh/proxy-examples)** - Example code for using proxies
- **[python-proxy-headers](https://github.com/proxymesh/python-proxy-headers)** — requests, urllib3, httpx, and aiohttp
- **[ruby-proxy-headers](https://github.com/proxymesh/ruby-proxy-headers)** — Net::HTTP, Faraday, HTTParty, Excon, and Typhoeus
- **[scrapy-proxy-headers](https://github.com/proxymesh/scrapy-proxy-headers)** — Scrapy download handler
- **[proxy-examples](https://github.com/proxymesh/proxy-examples)** — example proxy code

## About

Created by [ProxyMesh](https://proxymesh.com) to help our customers use custom headers to control proxy behavior. Works with any proxy that supports custom headers.

## License

MIT License
[MIT](https://github.com/proxymesh/javascript-proxy-headers/blob/main/LICENSE)
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,13 @@ We at [ProxyMesh](https://proxymesh.com) created these extension modules to supp
## Related Projects

- **[python-proxy-headers](https://github.com/proxymesh/python-proxy-headers)** - Same functionality for Python
- **[ruby-proxy-headers](https://github.com/proxymesh/ruby-proxy-headers)** - Same functionality for Ruby
- **[proxy-examples](https://github.com/proxymesh/proxy-examples)** - Example code for using proxies
- **[scrapy-proxy-headers](https://github.com/proxymesh/scrapy-proxy-headers)** - Proxy header support for Scrapy

## Requirements

- Node.js >= 18.0.0
- Node.js >= 22.19.0
- One or more supported HTTP libraries

## License
Expand Down
4 changes: 3 additions & 1 deletion docs/ky.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

[ky](https://github.com/sindresorhus/ky) is a small HTTP client built on `fetch`. This package wires ky to a custom `fetch` implemented with [node-fetch](node-fetch.md) and `ProxyHeadersAgent`, so CONNECT proxy headers work the same way as in the node-fetch adapter.

ky 1 receives that node-fetch response directly. ky 2 (including 2.2+, which throws `The fetch option must resolve with a Response` for a node-fetch `Response`) receives a global `Response` with the same `proxyHeaders` Map attached. `node-fetch` is still required.

## Getting Started

### Prerequisites
Expand Down Expand Up @@ -71,4 +73,4 @@ const res = await api.get('v1/status').json();

## Accessing Proxy Headers

Responses are [`ProxyResponse`](node-fetch.md) instances: use `response.proxyHeaders.get('x-proxymesh-ip')` (and the usual `response.json()`, `response.text()`, etc.). Proxy CONNECT failures use the same errors as the underlying fetch stack; see [Core API](core-api.md) for `ConnectError` on the agent.
Use `response.proxyHeaders.get('x-proxymesh-ip')` (and the usual `response.json()`, `response.text()`, etc.). On ky 2, `response` is a global `Response`; `proxyHeaders` is an own property and is not copied by `response.clone()`. Proxy CONNECT failures use the same errors as the underlying fetch stack; see [Core API](core-api.md) for `ConnectError` on the agent.
5 changes: 4 additions & 1 deletion docs/undici.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

[undici](https://undici.nodejs.org/) is a fast HTTP/1.1 client from the Node.js core team. It powers the native `fetch` in Node.js 18+. This page describes how to use undici with proxies and how to send and receive custom proxy headers.

HTTPS requests use a custom undici connector. The connector opens the proxy tunnel with `ProxyHeadersAgent` (custom CONNECT headers in, CONNECT response headers out) and hands undici the TLS socket only after CONNECT succeeds. Do not pass a pre-opened socket as `connect: { socket }`: undici's default connector ignores that option and dials the origin directly, which bypasses the proxy. Plain HTTP is forwarded with undici `ProxyAgent` and its `headers` option (undici PR #1877); that path does not expose a separate CONNECT response. Pass `requestTls` for origin TLS options such as `{ rejectUnauthorized: false }`. undici 8 requires Node.js >= 22.19.0.

## Getting Started

### Prerequisites
Expand Down Expand Up @@ -45,7 +47,8 @@ Make an HTTP request through a proxy with custom headers.
|------|------|-------------|
| `url` | `string \| URL` | Target URL |
| `options.proxy` | `string` | Proxy URL (required) |
| `options.proxyHeaders` | `Object` | Headers to send to the proxy |
| `options.proxyHeaders` | `Object` | Headers to send to the proxy on CONNECT |
| `options.requestTls` | `Object` | TLS options for the origin connection |
| `options.method` | `string` | HTTP method (default: 'GET') |
| `options.headers` | `Object` | Request headers |
| `options.body` | `*` | Request body |
Expand Down
4 changes: 2 additions & 2 deletions lib/axios-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import { getProxyHeaders } from './core/proxy-headers-store.js';
* @param {Object} options.proxyHeaders - Headers to send to the proxy
* @param {Function} options.onProxyConnect - Callback when CONNECT completes
* @param {Object} options.axiosOptions - Additional axios instance options
* @returns {import('axios').AxiosInstance}
* @returns {Promise<import('axios').AxiosInstance>}
*
* @example
* const client = createProxyAxios({
* const client = await createProxyAxios({
* proxy: 'http://user:pass@proxy:8080',
* proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
* });
Expand Down
4 changes: 2 additions & 2 deletions lib/got-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import { getProxyHeaders } from './core/proxy-headers-store.js';
* @param {Object} options.proxyHeaders - Headers to send to the proxy
* @param {Function} options.onProxyConnect - Callback when CONNECT completes
* @param {Object} options.gotOptions - Additional got instance options
* @returns {import('got').Got}
* @returns {Promise<import('got').Got>}
*
* @example
* const client = createProxyGot({
* const client = await createProxyGot({
* proxy: 'http://user:pass@proxy:8080',
* proxyHeaders: { 'X-ProxyMesh-Country': 'US' }
* });
Expand Down
Loading