proxy.ts
utils/proxy.ts
No strong subsystem tag
427
Lines
13545
Bytes
14
Exports
10
Imports
10
Keywords
What this is
This page documents one file from the repository and includes its full source so you can read it without leaving the docs site.
Beginner explanation
This file is one piece of the larger system. Its name, directory, imports, and exports show where it fits. Start by reading the exports and related files first.
How it is used
Start from the exports list and related files. Those are the easiest clues for where this file fits into the system.
Expert explanation
Architecturally, this file intersects with general runtime concerns. It contains 427 lines, 10 detected imports, and 14 detected exports.
Important relationships
Detected exports
disableKeepAlive_resetKeepAliveForTestinggetAddressFamilygetProxyUrlgetNoProxyshouldBypassProxycreateAxiosInstancegetProxyAgentgetWebSocketProxyAgentgetWebSocketProxyUrlgetProxyFetchOptionsconfigureGlobalAgentsgetAWSClientProxyConfigclearProxyCache
Keywords
proxyproxyurlconfigagentundicino_proxyaxiosoptionsmtlsmtlsconfig
Detected imports
axiosdnshttphttps-proxy-agentlodash-es/memoize.jsundici./caCerts.js./debug.js./envUtils.js./mtls.js
Source notes
This page embeds the full file contents. Small or leaf files are still indexed honestly instead of being over-explained.
Full source
// @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')
}