π― Use case
This file lives under βutils/β, which covers cross-cutting helpers (shell, tempfiles, settings, messages, process input, β¦). On the API surface it exposes disableKeepAlive, _resetKeepAliveForTesting, getAddressFamily, getProxyUrl, and getNoProxy (and more) β mainly functions, hooks, or classes. Dependencies touch HTTP client, dns, Node HTTP, and https-proxy-agent. It composes internal code from caCerts, debug, envUtils, and mtls (relative imports). What the file header says: @aws-sdk/credential-provider-node and @smithy/node-http-handler are imported dynamically in getAWSClientProxyConfig() to defer ~929KB of AWS SDK. undici is lazy-required inside getProxyAgent/configureGlobalAgents to defer ~1.5MB when no HTTPS_PROXY/mTLS env vars are set (the comm.
Generated from folder role, exports, dependency roots, and inline comments β not hand-reviewed for every path.
π§ Inline summary
@aws-sdk/credential-provider-node and @smithy/node-http-handler are imported dynamically in getAWSClientProxyConfig() to defer ~929KB of AWS SDK. undici is lazy-required inside getProxyAgent/configureGlobalAgents to defer ~1.5MB when no HTTPS_PROXY/mTLS env vars are set (the common case).
π€ Exports (heuristic)
disableKeepAlive_resetKeepAliveForTestinggetAddressFamilygetProxyUrlgetNoProxyshouldBypassProxycreateAxiosInstancegetProxyAgentgetWebSocketProxyAgentgetWebSocketProxyUrlgetProxyFetchOptionsconfigureGlobalAgentsgetAWSClientProxyConfigclearProxyCache
π External import roots
Package roots from from "β¦" (relative paths omitted).
axiosdnshttphttps-proxy-agentlodash-esundici
π₯οΈ Source preview
// @aws-sdk/credential-provider-node and @smithy/node-http-handler are imported
// dynamically in getAWSClientProxyConfig() to defer ~929KB of AWS SDK.
// undici is lazy-required inside getProxyAgent/configureGlobalAgents to defer
// ~1.5MB when no HTTPS_PROXY/mTLS env vars are set (the common case).
import axios, { type AxiosInstance } from 'axios'
import type { LookupOptions } from 'dns'
import type { Agent } from 'http'
import { HttpsProxyAgent, type HttpsProxyAgentOptions } from 'https-proxy-agent'
import memoize from 'lodash-es/memoize.js'
import type * as undici from 'undici'
import { getCACertificates } from './caCerts.js'
import { logForDebugging } from './debug.js'
import { isEnvTruthy } from './envUtils.js'
import {
getMTLSAgent,
getMTLSConfig,
getTLSFetchOptions,
type TLSConfig,
} from './mtls.js'
// Disable fetch keep-alive after a stale-pool ECONNRESET so retries open a
// fresh TCP connection instead of reusing the dead pooled socket. Sticky for
// the process lifetime β once the pool is known-bad, don't trust it again.
// Works under Bun (native fetch respects keepalive:false for pooling).
// Under Node/undici, keepalive is a no-op for pooling, but undici
// naturally evicts dead sockets from the pool on ECONNRESET.
let keepAliveDisabled = false
export function disableKeepAlive(): void {
keepAliveDisabled = true
}
export function _resetKeepAliveForTesting(): void {
keepAliveDisabled = false
}
/**
* Convert dns.LookupOptions.family to a numeric address family value
* Handles: 0 | 4 | 6 | 'IPv4' | 'IPv6' | undefined
*/
export function getAddressFamily(options: LookupOptions): 0 | 4 | 6 {
switch (options.family) {
case 0:
case 4:
case 6:
return options.family
case 'IPv6':
return 6
case 'IPv4':
case undefined:
return 4
default:
throw new Error(`Unsupported address family: ${options.family}`)
}
}
type EnvLike = Record<string, string | undefined>
/**
* Get the active proxy URL if one is configured
* Prefers lowercase variants over uppercase (https_proxy > HTTPS_PROXY > http_proxy > HTTP_PROXY)
* @param env Environment variables to check (defaults to process.env for production use)
*/
export function getProxyUrl(env: EnvLike = process.env): string | undefined {
return env.https_proxy || env.HTTPS_PROXY || env.http_proxy || env.HTTP_PROXY
}
/**
* Get the NO_PROXY environment variable value
* Prefers lowercase over uppercase (no_proxy > NO_PROXY)
* @param env Environment variables to check (defaults to process.env for production use)
*/
export function getNoProxy(env: EnvLike = process.env): string | undefined {
return env.no_proxy || env.NO_PROXY
}
/**
* Check if a URL should bypass the proxy based on NO_PROXY environment variable
* Supports:
* - Exact hostname matches (e.g., "localhost")
* - Domain suffix matches with leading dot (e.g., ".example.com")
* - Wildcard "*" to bypass all
* - Port-specific matches (e.g., "example.com:8080")
* - IP addresses (e.g., "127.0.0.1")
* @param urlString URL to check
* @param noProxy NO_PROXY value (defaults to getNoProxy() for production use)
*/
export function shouldBypassProxy(
urlString: string,
noProxy: string | undefined = getNoProxy(),
): boolean {
if (!noProxy) return false
// Handle wildcard
if (noProxy === '*') return true
try {
const url = new URL(urlString)
const hostname = url.hostname.toLowerCase()
const port = url.port || (url.protocol === 'https:' ? '443' : '80')
const hostWithPort = `${hostname}:${port}`
// Split by comma or space and trim each entry
const noProxyList = noProxy.split(/[,\s]+/).filter(Boolean)
return noProxyList.some(pattern => {
pattern = pattern.toLowerCase().trim()
// Check for port-specific match
if (pattern.includes(':')) {
return hostWithPort === pattern
}
// Check for domain suffix match (with or without leading dot)
if (pattern.startsWith('.')) {
// Pattern ".example.com" should match "sub.example.com" and "example.com"
// but NOT "notexample.com"
const suffix = pattern
return hostname === pattern.substring(1) || hostname.endsWith(suffix)
}
// Check for exact hostname match or IP address
return hostname === pattern
})
} catch {
// If URL parsing fails, don't bypass proxy
return false
}
}
/**
* Create an HttpsProxyAgent with optional mTLS configuration
* Skips local DNS resolution to let the proxy handle it
*/
function createHttpsProxyAgent(
proxyUrl: string,
extra: HttpsProxyAgentOptions<string> = {},
): HttpsProxyAgent<string> {
const mtlsConfig = getMTLSConfig()
const caCerts = getCACertificates()
const agentOptions: HttpsProxyAgentOptions<string> = {
...(mtlsConfig && {
cert: mtlsConfig.cert,
key: mtlsConfig.key,
passphrase: mtlsConfig.passphrase,
}),
...(caCerts && { ca: caCerts }),
}
if (isEnvTruthy(process.env.CLAUDE_CODE_PROXY_RESOLVES_HOSTS)) {
// Skip local DNS resolution - let the proxy resolve hostnames
// This is needed for environments where DNS is not configured locally
// and instead handled by the proxy (as in sandboxes)
agentOptions.lookup = (hostname, options, callback) => {
callback(null, hostname, getAddressFamily(options))
}
}
return new HttpsProxyAgent(proxyUrl, { ...agentOptions, ...extra })
}
/**
* Axios instance with its own proxy agent. Same NO_PROXY/mTLS/CA
* resolution as the global interceptor, but agent options stay
* scoped to this instance.
*/
export function createAxiosInstance(
extra: HttpsProxyAgentOptions<string> = {},
): AxiosInstance {
const proxyUrl = getProxyUrl()
const mtlsAgent = getMTLSAgent()
const instance = axios.create({ proxy: false })
if (!proxyUrl) {
if (mtlsAgent) instance.defaults.httpsAgent = mtlsAgent
return instance
}
const proxyAgent = createHttpsProxyAgent(proxyUrl, extra)
instance.interceptors.request.use(config => {
if (config.url && shouldBypassProxy(config.url)) {
config.httpsAgent = mtlsAgent
config.httpAgent = mtlsAgent
} else {
config.httpsAgent = proxyAgent
config.httpAgent = proxyAgent
}
return config
})
return instance
}
/**
* Get or create a memoized proxy agent for the given URI
* Now respects NO_PROXY environment variable
*/
export const getProxyAgent = memoize((uri: string): undici.Dispatcher => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const undiciMod = require('undici') as typeof undici
const mtlsConfig = getMTLSConfig()
const caCerts = getCACertificates()
// Use EnvHttpProxyAgent to respect NO_PROXY
// This agent automatically checks NO_PROXY for each request
const proxyOptions: undici.EnvHttpProxyAgent.Options & {
requestTls?: {
cert?: string | Buffer
key?: string | Buffer
passphrase?: string
ca?: string | string[] | Buffer
}
} = {
// Override both HTTP and HTTPS proxy with the provided URI
httpProxy: uri,
httpsProxy: uri,
noProxy: process.env.NO_PROXY || process.env.no_proxy,
}
// Set both connect and requestTls so TLS options apply to both paths:
// - requestTls: used by ProxyAgent for the TLS connection through CONNECT tunnels
// - connect: used by Agent for direct (no-proxy) connections
if (mtlsConfig || caCerts) {
const tlsOpts = {
...(mtlsConfig && {
cert: mtlsConfig.cert,
key: mtlsConfig.key,
passphrase: mtlsConfig.passphrase,
}),
...(caCerts && { ca: caCerts }),
}
proxyOptions.connect = tlsOpts
proxyOptions.requestTls = tlsOpts
}
return new undiciMod.EnvHttpProxyAgent(proxyOptions)
})
/**
* Get an HTTP agent configured for WebSocket proxy support
* Returns undefined if no proxy is configured or URL should bypass proxy
*/
export function getWebSocketProxyAgent(url: string): Agent | undefined {
const proxyUrl = getProxyUrl()
if (!proxyUrl) {
return undefined
}
// Check if URL should bypass proxy
if (shouldBypassProxy(url)) {
return undefined
}
return createHttpsProxyAgent(proxyUrl)
}
/**
* Get the proxy URL for WebSocket connections under Bun.
* Bun's native WebSocket supports a `proxy` string option instead of Node's `agent`.
* Returns undefined if no proxy is configured or URL should bypass proxy.
*/
export function getWebSocketProxyUrl(url: string): string | undefined {
const proxyUrl = getProxyUrl()
if (!proxyUrl) {
return undefined
}
if (shouldBypassProxy(url)) {
return undefined
}
return proxyUrl
}
/**
* Get fetch options for the Anthropic SDK with proxy and mTLS configuration
* Returns fetch options with appropriate dispatcher for proxy and/or mTLS
*
* @param opts.forAnthropicAPI - Enables ANTHROPIC_UNIX_SOCKET tunneling. This
* env var is set by `claude ssh` on the remote CLI to route API calls through
* an ssh -R forwarded unix socket to a local auth proxy. It MUST NOT leak
* into non-Anthropic-API fetch paths (MCP HTTP/SSE transports, etc.) or those
* requests get misrouted to api.anthropic.com. Only the Anthropic SDK client
* should pass `true` here.
*/
export function getProxyFetchOptions(opts?: { forAnthropicAPI?: boolean }): {
tls?: TLSConfig
dispatcher?: undici.Dispatcher
proxy?: string
unix?: string
keepalive?: false
} {
const base = keepAliveDisabled ? ({ keepalive: false } as const) : {}
// ANTHROPIC_UNIX_SOCKET tunnels through the `claude ssh` auth proxy, which
// hardcodes the upstream to the Anthropic API. Scope to the Anthropic API
// client so MCP/SSE/other callers don't get their requests misrouted.
if (opts?.forAnthropicAPI) {
const unixSocket = process.env.ANTHROPIC_UNIX_SOCKET
if (unixSocket && typeof Bun !== 'undefined') {
return { ...base, unix: unixSocket }
}
}
const proxyUrl = getProxyUrl()
// If we have a proxy, use the proxy agent (which includes mTLS config)
if (proxyUrl) {
if (typeof Bun !== 'undefined') {
return { ...base, proxy: proxyUrl, ...getTLSFetchOptions() }
}
return { ...base, dispatcher: getProxyAgent(proxyUrl) }
}
// Otherwise, use TLS options directly if available
return { ...base, ...getTLSFetchOptions() }
}
/**
* Configure global HTTP agents for both axios and undici
* This ensures all HTTP requests use the proxy and/or mTLS if configured
*/
let proxyInterceptorId: number | undefined
export function configureGlobalAgents(): void {
const proxyUrl = getProxyUrl()
const mtlsAgent = getMTLSAgent()
// Eject previous interceptor to avoid stacking on repeated calls
if (proxyInterceptorId !== undefined) {
axios.interceptors.request.eject(proxyInterceptorId)
proxyInterceptorId = undefined
}
// Reset proxy-related defaults so reconfiguration is clean
axios.defaults.proxy = undefined
axios.defaults.httpAgent = undefined
axios.defaults.httpsAgent = undefined
if (proxyUrl) {
// workaround for https://github.com/axios/axios/issues/4531
axios.defaults.proxy = false
// Create proxy agent with mTLS options if available
const proxyAgent = createHttpsProxyAgent(proxyUrl)
// Add axios request interceptor to handle NO_PROXY
proxyInterceptorId = axios.interceptors.request.use(config => {
// Check if URL should bypass proxy based on NO_PROXY
if (config.url && shouldBypassProxy(config.url)) {
// Bypass proxy - use mTLS agent if configured, otherwise undefined
if (mtlsAgent) {
config.httpsAgent = mtlsAgent
config.httpAgent = mtlsAgent
} else {
// Remove any proxy agents to use direct connection
delete config.httpsAgent
delete config.httpAgent
}
} else {
// Use proxy agent
config.httpsAgent = proxyAgent
config.httpAgent = proxyAgent
}
return config
})
// Set global dispatcher that now respects NO_PROXY via EnvHttpProxyAgent
// eslint-disable-next-line @typescript-eslint/no-require-imports
;(require('undici') as typeof undici).setGlobalDispatcher(
getProxyAgent(proxyUrl),
)
} else if (mtlsAgent) {
// No proxy but mTLS is configured
axios.defaults.httpsAgent = mtlsAgent
// Set undici global dispatcher with mTLS
const mtlsOptions = getTLSFetchOptions()
if (mtlsOptions.dispatcher) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
;(require('undici') as typeof undici).setGlobalDispatcher(
mtlsOptions.dispatcher,
)
}
}
}
/**
* Get AWS SDK client configuration with proxy support
* Returns configuration object that can be spread into AWS service client constructors
*/
export async function getAWSClientProxyConfig(): Promise<object> {
const proxyUrl = getProxyUrl()
if (!proxyUrl) {
return {}
}
const [{ NodeHttpHandler }, { defaultProvider }] = await Promise.all([
import('@smithy/node-http-handler'),
import('@aws-sdk/credential-provider-node'),
])
const agent = createHttpsProxyAgent(proxyUrl)
const requestHandler = new NodeHttpHandler({
httpAgent: agent,
httpsAgent: agent,
})
return {
requestHandler,
credentials: defaultProvider({
clientConfig: { requestHandler },
}),
}
}
/**
* Clear proxy agent cache.
*/
export function clearProxyCache(): void {
getProxyAgent.cache.clear?.()
logForDebugging('Cleared proxy agent cache')
}