undici-proxy-env 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/index.d.ts +24 -0
- package/index.mjs +78 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 oratis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# undici-proxy-env
|
|
2
|
+
|
|
3
|
+
Your OpenAI / Anthropic / any-SDK call **silently fails behind a proxy** — but `curl` works fine? It's because **Node's built-in `fetch` (undici) ignores `HTTPS_PROXY` / `HTTP_PROXY`.** One call fixes it — plus the two response-mangling gotchas that local proxies like **Clash** introduce.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm i undici-proxy-env
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { configureProxyFromEnv } from "undici-proxy-env";
|
|
11
|
+
|
|
12
|
+
// Call once, before any fetch/SDK use. Reads HTTPS_PROXY/HTTP_PROXY.
|
|
13
|
+
configureProxyFromEnv(); // → true if a proxy is now in effect
|
|
14
|
+
|
|
15
|
+
// ...now the Anthropic/OpenAI SDK, LangChain, plain fetch, etc. go through it.
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## The three problems it solves
|
|
19
|
+
|
|
20
|
+
**1. `fetch` ignores the proxy env vars.** Node's `http`-module clients and `curl` honor `HTTPS_PROXY`; `fetch`/undici do not. So the SDK (which uses `fetch`) times out or 403s behind a corporate/region proxy while everything else works. `configureProxyFromEnv()` installs a global undici `ProxyAgent` from the env — every subsequent `fetch` goes through it.
|
|
21
|
+
|
|
22
|
+
**2. Clash strips `Content-Encoding` but sends gzip.** Some local proxies (Clash, others) drop the `Content-Encoding: gzip` header through CONNECT tunnels while still sending gzipped bytes — undici then hands your SDK **raw gzip** that fails to JSON-parse. We force `Accept-Encoding: identity` outbound so the upstream returns plain text.
|
|
23
|
+
|
|
24
|
+
**3. Clash drops the response `Content-Type`.** Without it, an SDK treats a JSON body as a string and `messages.create()` returns text instead of a parsed object. `proxyAwareFetch` re-injects `application/json` when the body looks like JSON:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { configureProxyFromEnv, proxyAwareFetch } from "undici-proxy-env";
|
|
28
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
29
|
+
|
|
30
|
+
configureProxyFromEnv();
|
|
31
|
+
const client = new Anthropic({ fetch: proxyAwareFetch }); // survives header-stripping proxies
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
(If your proxy doesn't strip headers you don't need `proxyAwareFetch` — the global agent from step 1 is enough. It's a safe pass-through either way.)
|
|
35
|
+
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
| Export | What it does |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `configureProxyFromEnv(opts?)` | Install the global proxy from env. `opts.url` forces a URL; `opts.log(msg)` gets a one-line notice. Idempotent. Returns `boolean` (is a proxy now active). |
|
|
41
|
+
| `isProxyInstalled()` | `true` iff a proxy was installed. |
|
|
42
|
+
| `proxyAwareFetch` | A `fetch` drop-in that repairs a missing response `Content-Type`. Pass as an SDK's `fetch` option. |
|
|
43
|
+
|
|
44
|
+
Node ≥ 18. Zero config beyond the standard `HTTPS_PROXY` / `HTTP_PROXY` env vars.
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT © oratis
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ConfigureProxyOptions {
|
|
2
|
+
/** Force a specific proxy URL instead of reading the environment. */
|
|
3
|
+
url?: string;
|
|
4
|
+
/** Called with a one-line notice when a proxy is installed (or fails to). */
|
|
5
|
+
log?: (message: string) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Install a global undici ProxyAgent from HTTPS_PROXY / HTTP_PROXY (and lowercase
|
|
10
|
+
* variants), forcing `Accept-Encoding: identity` on outbound requests. No-op and
|
|
11
|
+
* returns `false` if no proxy is configured. Idempotent.
|
|
12
|
+
* @returns whether a proxy is now in effect.
|
|
13
|
+
*/
|
|
14
|
+
export function configureProxyFromEnv(opts?: ConfigureProxyOptions): boolean;
|
|
15
|
+
|
|
16
|
+
/** True iff configureProxyFromEnv installed a proxy. */
|
|
17
|
+
export function isProxyInstalled(): boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A `fetch` drop-in that re-injects a missing `application/json` response
|
|
21
|
+
* Content-Type (a Clash CONNECT-tunnel artifact) so SDKs parse JSON instead of
|
|
22
|
+
* returning a string. Pass-through when no proxy is installed.
|
|
23
|
+
*/
|
|
24
|
+
export const proxyAwareFetch: typeof fetch;
|
package/index.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// undici-proxy-env — make Node's built-in fetch honor HTTP(S)_PROXY, and survive
|
|
2
|
+
// the response-mangling that local proxies (Clash, some corporate proxies) do.
|
|
3
|
+
//
|
|
4
|
+
// Node's global `fetch` (undici) does NOT read HTTPS_PROXY / HTTP_PROXY. So an
|
|
5
|
+
// SDK that uses fetch under the hood (Anthropic, OpenAI, many others) silently
|
|
6
|
+
// fails behind a proxy even when your shell `curl` works. Call
|
|
7
|
+
// configureProxyFromEnv() once at startup to bridge that.
|
|
8
|
+
//
|
|
9
|
+
// Two real-world proxy artifacts are handled:
|
|
10
|
+
// 1. Some proxies strip `Content-Encoding: gzip` on CONNECT tunnels but still
|
|
11
|
+
// send gzipped bytes — undici then hands the SDK raw gzip that fails to
|
|
12
|
+
// JSON-parse. We force `Accept-Encoding: identity` outbound to avoid it.
|
|
13
|
+
// 2. Some proxies drop the response `Content-Type` header — an SDK then treats
|
|
14
|
+
// a JSON body as text and returns a string instead of a parsed object.
|
|
15
|
+
// `proxyAwareFetch` re-injects `application/json` when the body looks like JSON.
|
|
16
|
+
import { ProxyAgent, setGlobalDispatcher } from "undici";
|
|
17
|
+
|
|
18
|
+
let installed = false;
|
|
19
|
+
|
|
20
|
+
class IdentityEncodingProxyAgent extends ProxyAgent {
|
|
21
|
+
dispatch(opts, handler) {
|
|
22
|
+
const incoming = opts.headers ?? {};
|
|
23
|
+
const merged = {};
|
|
24
|
+
if (Array.isArray(incoming)) {
|
|
25
|
+
for (let i = 0; i < incoming.length; i += 2) merged[String(incoming[i]).toLowerCase()] = String(incoming[i + 1]);
|
|
26
|
+
} else if (typeof incoming === "object") {
|
|
27
|
+
for (const [k, v] of Object.entries(incoming)) merged[k.toLowerCase()] = Array.isArray(v) ? v.join(", ") : String(v);
|
|
28
|
+
}
|
|
29
|
+
merged["accept-encoding"] = "identity";
|
|
30
|
+
return super.dispatch({ ...opts, headers: merged }, handler);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Install a global undici ProxyAgent from HTTPS_PROXY/HTTP_PROXY (and lowercase
|
|
36
|
+
* variants). No-op + returns false if none is set. Idempotent. Pass `{ url }` to
|
|
37
|
+
* force a specific proxy, or `{ log }` to get a one-line notice.
|
|
38
|
+
* @returns {boolean} whether a proxy is now in effect.
|
|
39
|
+
*/
|
|
40
|
+
export function configureProxyFromEnv(opts = {}) {
|
|
41
|
+
const url =
|
|
42
|
+
opts.url ??
|
|
43
|
+
process.env.HTTPS_PROXY ?? process.env.https_proxy ??
|
|
44
|
+
process.env.HTTP_PROXY ?? process.env.http_proxy;
|
|
45
|
+
if (!url) return false;
|
|
46
|
+
if (installed) return true;
|
|
47
|
+
try {
|
|
48
|
+
setGlobalDispatcher(new IdentityEncodingProxyAgent(url));
|
|
49
|
+
installed = true;
|
|
50
|
+
opts.log?.(`[proxy] outbound fetch routed through ${url} (Accept-Encoding=identity)`);
|
|
51
|
+
return true;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
opts.log?.(`[proxy] failed to install ProxyAgent for ${url}: ${(err && err.message) || err}`);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** True iff configureProxyFromEnv installed a proxy. */
|
|
59
|
+
export function isProxyInstalled() {
|
|
60
|
+
return installed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* A `fetch` drop-in that repairs a missing response Content-Type (a Clash CONNECT
|
|
65
|
+
* artifact) so SDKs parse JSON instead of returning a string. Safe when no proxy
|
|
66
|
+
* is installed — it passes through untouched. Pass it as an SDK's `fetch` option.
|
|
67
|
+
* @type {typeof fetch}
|
|
68
|
+
*/
|
|
69
|
+
export const proxyAwareFetch = async (input, init) => {
|
|
70
|
+
const r = await fetch(input, init);
|
|
71
|
+
if (!installed) return r;
|
|
72
|
+
if (r.headers.get("content-type")) return r;
|
|
73
|
+
const text = await r.text();
|
|
74
|
+
const looksJson = text.trimStart().startsWith("{") || text.trimStart().startsWith("[");
|
|
75
|
+
const headers = new Headers(r.headers);
|
|
76
|
+
if (looksJson) headers.set("content-type", "application/json");
|
|
77
|
+
return new Response(text, { status: r.status, statusText: r.statusText, headers });
|
|
78
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "undici-proxy-env",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Make Node's built-in fetch honor HTTP(S)_PROXY — and survive the Clash/corporate-proxy response mangling that silently breaks LLM SDKs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.mjs",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"default": "./index.mjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.mjs",
|
|
16
|
+
"index.d.ts",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"undici",
|
|
22
|
+
"fetch",
|
|
23
|
+
"proxy",
|
|
24
|
+
"https_proxy",
|
|
25
|
+
"http_proxy",
|
|
26
|
+
"clash",
|
|
27
|
+
"anthropic",
|
|
28
|
+
"openai",
|
|
29
|
+
"llm",
|
|
30
|
+
"node"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"undici": "^8.5.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "node --test"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"author": "oratis",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/oratis/undici-proxy-env.git"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/oratis/undici-proxy-env#readme"
|
|
48
|
+
}
|