@jameskh/proxywire
v2.0.0
Published
Proxy list parsing and proxy-aware fetch for Node and Bun. Normalizes provider export formats, builds HTTP/SOCKS dispatchers, and returns a fetch that actually goes through the proxy on both runtimes.
Maintainers
Readme
proxywire
Proxy list parsing and proxy-aware fetch for Node and Bun.
Two jobs, both of which tend to get reinvented badly:
- Turn whatever your provider exported into a URL you can dial. Residential
proxy vendors ship lists in at least five mutually incompatible notations.
proxywireaccepts all of them and hands back one normalized URL. - Give you a
fetchthat actually goes through the proxy. On both runtimes. This is less obvious than it sounds — see Bun below.
No framework, no globals, no logging to the console. Node 18+ or Bun.
Install
bun add @jameskh/proxywire # or: npm i @jameskh/proxywireQuick start
import { loadProxyFile, makeFetcher, maskProxy, shuffleProxies } from "@jameskh/proxywire";
const proxies = shuffleProxies(loadProxyFile("./proxies.txt"));
for (const proxy of proxies) {
const { fetch, close } = makeFetcher(proxy, { timeoutMs: 45_000 });
try {
const response = await fetch("https://api.ipify.org?format=json");
console.log(maskProxy(proxy), await response.json());
break;
} finally {
await close();
}
}Proxy notations
normalizeProxy accepts every shape below and returns a dialable URL. Anything
it cannot understand throws a ProxyParseError naming the source and line.
| Input | Result |
| --- | --- |
| socks5://bob:[email protected]:1080 | unchanged |
| socks5://1.2.3.4:1080:bob:pw | socks5://bob:[email protected]:1080 |
| bob:[email protected]:1080 | socks5://bob:[email protected]:1080 |
| 1.2.3.4:1080@bob:pw | socks5://bob:[email protected]:1080 |
| bob:pw:1.2.3.4:1080 | socks5://bob:[email protected]:1080 |
| 1.2.3.4:1080:bob:pw | socks5://bob:[email protected]:1080 |
The last four carry no scheme, so one is applied — socks5 unless you say
otherwise. Host and credentials are told apart by which field looks like a port,
and credentials are percent-encoded so a password containing @ or : cannot
silently rewrite the host.
normalizeProxy("bob:[email protected]:1080", { defaultProtocol: "http" });
// Resolve the scheme lazily when it comes from the environment, so a variable
// set after import time is still honoured.
normalizeProxy(line, { defaultProtocol: () => process.env.PROXY_PROTOCOL ?? "socks5" });Supported schemes: http, https, socks4, socks5.
Reading a list
import { loadProxyFile, parseProxyList } from "@jameskh/proxywire";
loadProxyFile("./config/proxies.txt"); // [] when the file is absent
parseProxyList(text, { source: "PROXY_ENV" }); // same parser, any stringBlank lines and # comments are skipped, but line numbers are counted against
the original text, so errors point at the line you actually have to fix.
Lists still holding the placeholder rows that ship in example files
(user:pass@host:port and friends) are rejected outright — running against a
template is a configuration mistake, and failing on it beats a confusing DNS
error later. Pass rejectPlaceholders: false to skip the check, or
commentPrefix to use something other than #.
Making requests
const { fetch, dispatcher, close } = makeFetcher(proxy, {
timeoutMs: 30_000, // applied unless you pass your own signal
socksConnectTimeoutMs: 15_000,
minTlsVersion: "TLSv1.2",
logger: { debug: (m) => log.debug(m) },
});Call close() when you are finished. Without it every request cycle leaks an
undici Agent and its sockets, which exhausts file descriptors in any
long-running process. makeFetcher() with no proxy returns a plain fetch and
a close() that does nothing, so the calling code needs no branching.
SOCKS proxies get an undici Agent whose connect opens the tunnel by hand and
starts TLS over it, since undici has no SOCKS support of its own. HTTP and HTTPS
proxies go to ProxyAgent.
Bun
Bun ignores undici's dispatcher option. A request aimed at a dead proxy still
succeeds and reports the machine's own address — every "proxied" call silently
goes out unproxied, which is the worst possible failure mode for anything that
uses proxies for a reason. Bun's own fetch takes a proxy option that it does
honour, so makeFetcher uses that path under Bun and the dispatcher path under
Node.
Bun's proxy option cannot do SOCKS. Rather than fall through to a dispatcher
Bun will ignore, makeFetcher throws:
SOCKS proxies are not supported when running under Bun (1.2.3.4:1080).
Use an http:// proxy, or set allowSocksUnderBun to opt out of this check.The proxy is masked to host:port in that message — credentials in a proxy URL
are as sensitive as any other password and must never reach a log.
Diagnosing failures
fetch throws a bare TypeError: fetch failed for every network failure and
puts the real reason in cause, so a dead proxy, a bad credential and a slow
host are indistinguishable. describeError flattens the chain:
describeError(error); // "fetch failed -> connection refused (ECONNREFUSED)"retryOnTransportFailure reruns an operation once on failure, which is enough
for the connection drops a rotating pool produces on its own:
await retryOnTransportFailure("token request", () => fetch(url, init), { logger });API
| Export | Purpose |
| --- | --- |
| makeFetcher(proxy?, options?) | { fetch, dispatcher?, close() } bound to one proxy |
| makeProxyDispatcher(proxy, options?) | The undici dispatcher on its own |
| closeDispatcher(dispatcher?) | Release a dispatcher's socket pool |
| withRequestTimeout(init, ms) | Attach a timeout signal unless one exists |
| retryOnTransportFailure(label, run, options?) | Run once more on failure |
| normalizeProxy(input, options?) | Any notation to a dialable URL |
| parseProxyList(text, options?) | Newline-separated list to URLs |
| loadProxyFile(path, options?) | Same, from a file; [] when absent |
| parseSchemelessProxy(input) | The four schemeless shapes, as parts |
| isPlaceholderProxy(input) | True for example-file template rows |
| maskProxy(proxy) | host:port, safe to log |
| shuffleProxies(list) | Fisher-Yates copy, to spread load |
| describeError(error) | Flatten an error and its cause chain |
| IS_BUN | Runtime check used for the paths above |
| SUPPORTED_PROXY_PROTOCOLS | ["http", "https", "socks4", "socks5"] |
| ProxyParseError | Carries source and lineNumber |
Types: Fetcher, FetcherOptions, ProxyDispatcher, ProxyFetch,
ProxyListOptions, ProxyLogger, ProxyParseOptions, ProxyProtocol.
Runtime support
The published package ships both compiled JavaScript and its TypeScript source.
Node resolves dist/ with full .d.ts declarations; Bun resolves the bun
export condition and reads src/ directly, so a linked checkout picks up edits
with no build step.
Development
bun install
bun test
bun run typecheck
bun run build # emits dist/ for Node consumersRelative imports carry explicit .js extensions so the emitted ESM resolves
under Node. TypeScript maps ./errors.js back to errors.ts at compile time.
License
MIT
