// @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 /** * 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 = {}, ): HttpsProxyAgent { const mtlsConfig = getMTLSConfig() const caCerts = getCACertificates() const agentOptions: HttpsProxyAgentOptions = { ...(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 = {}, ): 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 { 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') }