/ src / utils / apiPreconnect.ts
apiPreconnect.ts
 1  /**
 2   * Preconnect to the Anthropic API to overlap TCP+TLS handshake with startup.
 3   *
 4   * The TCP+TLS handshake is ~100-200ms that normally blocks inside the first
 5   * API call. Kicking a fire-and-forget fetch during init lets the handshake
 6   * happen in parallel with action-handler work (~100ms of setup/commands/mcp
 7   * before the API request in -p mode; unbounded "user is typing" window in
 8   * interactive mode).
 9   *
10   * Bun's fetch shares a keep-alive connection pool globally, so the real API
11   * request reuses the warmed connection.
12   *
13   * Called from init.ts AFTER applyExtraCACertsFromConfig() + configureGlobalAgents()
14   * so settings.json env vars are applied and the TLS cert store is finalized.
15   * The early cli.tsx call site was removed — it ran before settings.json loaded,
16   * so ANTHROPIC_BASE_URL/proxy/mTLS in settings would be invisible and preconnect
17   * would warm the wrong pool (or worse, lock BoringSSL's cert store before
18   * NODE_EXTRA_CA_CERTS was applied).
19   *
20   * Skipped when:
21   * - proxy/mTLS/unix socket configured (preconnect would use wrong transport —
22   *   the SDK passes a custom dispatcher/agent that doesn't share the global pool)
23   * - Bedrock/Vertex/Foundry (different endpoints, different auth)
24   */
25  
26  import { getOauthConfig } from '../constants/oauth.js'
27  import { isEnvTruthy } from './envUtils.js'
28  
29  let fired = false
30  
31  export function preconnectAnthropicApi(): void {
32    if (fired) return
33    fired = true
34  
35    // Skip if using a cloud provider — different endpoint + auth
36    if (
37      isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
38      isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
39      isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY)
40    ) {
41      return
42    }
43    // Skip if proxy/mTLS/unix — SDK's custom dispatcher won't reuse this pool
44    if (
45      process.env.HTTPS_PROXY ||
46      process.env.https_proxy ||
47      process.env.HTTP_PROXY ||
48      process.env.http_proxy ||
49      process.env.ANTHROPIC_UNIX_SOCKET ||
50      process.env.CLAUDE_CODE_CLIENT_CERT ||
51      process.env.CLAUDE_CODE_CLIENT_KEY
52    ) {
53      return
54    }
55  
56    // Use configured base URL (staging, local, or custom gateway). Covers
57    // ANTHROPIC_BASE_URL env + USE_STAGING_OAUTH + USE_LOCAL_OAUTH in one lookup.
58    // NODE_EXTRA_CA_CERTS no longer a skip — init.ts applied it before this fires.
59    const baseUrl =
60      process.env.ANTHROPIC_BASE_URL || getOauthConfig().BASE_API_URL
61  
62    // Fire and forget. HEAD means no response body — the connection is eligible
63    // for keep-alive pool reuse immediately after headers arrive. 10s timeout
64    // so a slow network doesn't hang the process; abort is fine since the real
65    // request will handshake fresh if needed.
66    // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
67    void fetch(baseUrl, {
68      method: 'HEAD',
69      signal: AbortSignal.timeout(10_000),
70    }).catch(() => {})
71  }