Skip to content

Commit d2d7588

Browse files
authored
feat(proxy): support no_proxy (#109)
1 parent 742d27e commit d2d7588

File tree

3 files changed

+83
-29
lines changed

3 files changed

+83
-29
lines changed

README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Sometimes you want to explicitly use none native (`node-fetch`) implementation o
8686

8787
You have two ways to do this:
8888

89-
- Set `FORCE_NODE_FETCH` environment variable before starting the application.
89+
- Set the `FORCE_NODE_FETCH` environment variable before starting the application.
9090
- Import from `node-fetch-native/node`
9191

9292
## Polyfill support
@@ -111,7 +111,9 @@ Node.js has no built-in support for HTTP Proxies for fetch (see [nodejs/undici#1
111111

112112
This package bundles a compact and simple proxy-supported solution for both Node.js versions without native fetch using [HTTP Agent](https://github.com/TooTallNate/proxy-agents/tree/main/packages/proxy-agent) and versions with native fetch using [Undici Proxy Agent](https://undici.nodejs.org/#/docs/api/ProxyAgent).
113113

114-
By default, `https_proxy`, `http_proxy`, `HTTPS_PROXY`, and `HTTP_PROXY` environment variables will be checked and used (in order) for the proxy and if not any of them are set, the proxy will be disabled.
114+
By default, `https_proxy`, `http_proxy`, `HTTPS_PROXY`, and `HTTP_PROXY` environment variables will be checked and used (in order) for the proxy and if not any of them are set, the proxy will be disabled. You can override it using the `url` option passed to `createFetch` and `createProxy` utils.
115+
116+
By default, `no_proxy` and `NO_PROXY` environment variables will be checked and used for the (comma-separated) list of hosts to ignore the proxy for. You can override it using the `noProxy` option passed to `createFetch` and `createProxy` utils. The entries starting with a dot will be used to check the domain and also any subdomain.
115117

116118
> [!NOTE]
117119
> Using export conditions, this utility adds proxy support for Node.js and for other runtimes, it will simply return native fetch.
@@ -131,7 +133,7 @@ console.log(await fetch("https://icanhazip.com").then((r) => r.text());
131133
132134
### `createFetch` utility
133135
134-
You can use `createFetch` utility to intantiate a `fetch` instance with custom proxy options.
136+
You can use the `createFetch` utility to instantiate a `fetch` instance with custom proxy options.
135137
136138
```ts
137139
import { createFetch } from "node-fetch-native/proxy";

lib/proxy.d.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,23 @@ import type * as http from "node:http";
22
import type * as https from "node:https";
33
import type * as undici from "undici";
44

5-
export type ProxyOptions = { url?: string };
5+
export type ProxyOptions = {
6+
/**
7+
* HTTP(s) Proxy URL
8+
*
9+
* Default is read from `https_proxy`, `http_proxy`, `HTTPS_PROXY` or `HTTP_PROXY` environment variables
10+
* */
11+
url?: string;
12+
13+
/**
14+
* List of hosts to skip proxy for (comma separated or array of strings)
15+
*
16+
* Default is read from `no_proxy` or `NO_PROXY` environment variables
17+
*
18+
* Hots starting with a leading dot, like `.foo.com` are also matched against domain and all its subdomains like `bar.foo.com`
19+
*/
20+
noProxy?: string | string[];
21+
};
622

723
export declare const createProxy: (opts?: ProxyOptions) => {
824
agent: http.Agent | https.Agent | undefined;

src/proxy.ts

+61-25
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as http from "node:http";
22
import * as https from "node:https";
33
import { URL } from "node:url";
4-
import { ProxyAgent as UndiciProxyAgent } from "undici";
4+
import { Agent as _UndiciAgent, ProxyAgent as _UndiciProxyAgent } from "undici";
55
import { Agent, AgentConnectOpts } from "agent-base";
66
import { HttpProxyAgent } from "http-proxy-agent";
77
import { HttpsProxyAgent } from "https-proxy-agent";
@@ -23,10 +23,16 @@ export function createProxy(opts: ProxyOptions = {}) {
2323
};
2424
}
2525

26-
const nodeAgent = new NodeProxyAgent({ uri });
26+
const _noProxy = opts.noProxy || process.env.no_proxy || process.env.NO_PROXY;
27+
const noProxy = typeof _noProxy === "string" ? _noProxy.split(",") : _noProxy;
28+
29+
const nodeAgent = new NodeProxyAgent({ uri, noProxy });
2730

2831
// https://undici.nodejs.org/#/docs/api/ProxyAgent
29-
const undiciAgent = new UndiciProxyAgent({ uri });
32+
const undiciAgent = new UndiciProxyAgent({
33+
uri,
34+
noProxy,
35+
});
3036

3137
return {
3238
agent: nodeAgent,
@@ -45,9 +51,47 @@ export const fetch = createFetch({});
4551
// Utils
4652
// ----------------------------------------------
4753

48-
export function debug(...args: any[]) {
49-
if (process.env.debug) {
50-
debug("[node-fetch-native] [proxy]", ...args);
54+
function debug(...args: any[]) {
55+
if (process.env.DEBUG) {
56+
console.debug("[node-fetch-native] [proxy]", ...args);
57+
}
58+
}
59+
60+
function bypassProxy(host: string, noProxy: string[]) {
61+
if (!noProxy) {
62+
return false;
63+
}
64+
for (const _host of noProxy) {
65+
if (_host === host || (_host[0] === "." && host.endsWith(_host.slice(1)))) {
66+
return true;
67+
}
68+
}
69+
return false;
70+
}
71+
72+
// ----------------------------------------------
73+
// Undici Agent
74+
// ----------------------------------------------
75+
76+
// https://github.com/nodejs/undici/blob/main/lib/proxy-agent.js
77+
78+
class UndiciProxyAgent extends _UndiciProxyAgent {
79+
_agent: _UndiciAgent;
80+
81+
constructor(
82+
private _options: _UndiciProxyAgent.Options & { noProxy: string[] },
83+
) {
84+
super(_options);
85+
this._agent = new _UndiciAgent();
86+
}
87+
88+
dispatch(options, handler): boolean {
89+
const hostname = new URL(options.origin).hostname;
90+
if (bypassProxy(hostname, this._options.noProxy)) {
91+
debug(`Bypassing proxy for: ${hostname}`);
92+
return this._agent.dispatch(options, handler);
93+
}
94+
return super.dispatch(options, handler);
5195
}
5296
}
5397

@@ -73,15 +117,14 @@ function isValidProtocol(v: string): v is ValidProtocol {
73117
return (PROTOCOLS as readonly string[]).includes(v);
74118
}
75119

76-
export class NodeProxyAgent extends Agent {
120+
class NodeProxyAgent extends Agent {
77121
cache: Map<string, Agent> = new Map();
78122

79123
httpAgent: http.Agent;
80124
httpsAgent: http.Agent;
81125

82-
constructor(private proxyOptions: { uri: string }) {
126+
constructor(private _options: { uri: string; noProxy: string[] }) {
83127
super({});
84-
debug("Creating new ProxyAgent instance: %o", proxyOptions);
85128
this.httpAgent = new http.Agent({});
86129
this.httpsAgent = new https.Agent({});
87130
}
@@ -94,33 +137,26 @@ export class NodeProxyAgent extends Agent {
94137
? (isWebSocket ? "wss:" : "https:")
95138
: (isWebSocket ? "ws:" : "http:");
96139

97-
const host = req.getHeader("host");
98-
const url = new URL(req.path, `${protocol}//${host}`).href;
99-
const proxy = this.proxyOptions.uri;
140+
const host = req.getHeader("host") as string;
100141

101-
if (!proxy) {
102-
debug("Proxy not enabled for URL: %o", url);
142+
if (bypassProxy(host, this._options.noProxy)) {
103143
return opts.secureEndpoint ? this.httpsAgent : this.httpAgent;
104144
}
105145

106-
debug("Request URL: %o", url);
107-
debug("Proxy URL: %o", proxy);
108-
109146
// Attempt to get a cached `http.Agent` instance first
110-
const cacheKey = `${protocol}+${proxy}`;
147+
const cacheKey = `${protocol}+${this._options.uri}`;
111148
let agent = this.cache.get(cacheKey);
112-
if (agent) {
113-
debug("Cache hit for proxy URL: %o", proxy);
114-
} else {
115-
const proxyUrl = new URL(proxy);
149+
if (!agent) {
150+
const proxyUrl = new URL(this._options.uri);
116151
const proxyProto = proxyUrl.protocol.replace(":", "");
117152
if (!isValidProtocol(proxyProto)) {
118-
throw new Error(`Unsupported protocol for proxy URL: ${proxy}`);
153+
throw new Error(
154+
`Unsupported protocol for proxy URL: ${this._options.uri}`,
155+
);
119156
}
120157
const Ctor =
121158
proxies[proxyProto][opts.secureEndpoint || isWebSocket ? 1 : 0];
122-
// @ts-expect-error meh…
123-
agent = new Ctor(proxy, this.connectOpts);
159+
agent = new (Ctor as any)(this._options.uri, this._options);
124160
this.cache.set(cacheKey, agent);
125161
}
126162

0 commit comments

Comments
 (0)