Skip to content

Claude Code: API Error: The socket connection was closed unexpectedly when attached to ai-gateway #93

@bgmcmullen

Description

@bgmcmullen

Symptom

When Claude Code is attached to the HypAware AI gateway (ANTHROPIC_BASE_URL pointed at the local
proxy), sessions intermittently show:

⏺ API Error: The socket connection was closed unexpectedly. For more information, pass `verbose:
true` in the second argument to fetch().

This is the client-side error undici/fetch raises when the TCP socket to the gateway is closed
mid-request or mid-SSE-stream.

Environment

  • hyp 1.2.0, daemon running (mode=foreground), plugins: @hypaware/ai-gateway, @hypaware/claude,
    @hypaware/codex, @hypaware/local-fs, @hypaware/format-parquet
  • Claude Code 2.1.145–2.1.168 (per recorded client_version)
  • macOS, Darwin 24.6.0

Evidence from local recordings

Distinct gateway exchanges with errors on the anthropic upstream since 2026-06-01:

gateway error is_sse exchanges
client_aborted true 16
client_aborted false 15
socket hang up false 2
read ECONNRESET true 1
read ETIMEDOUT false 1

The read ECONNRESET case is the smoking gun for the visible error: it happened on an SSE exchange
(/v1/messages?beta=true) with status_code: 200 already sent and only 856 response bytes recorded
— i.e. the upstream connection reset mid-stream after headers were forwarded, so the gateway had no
way to return an error status and destroyed the client socket instead.

The socket hang up cases occurred before headers were sent and were correctly converted to a 502
JSON response (response_bytes: 0), which Claude Code can retry — those are handled fine.

Query used:

hyp query sql "SELECT CAST(json_extract(attributes, '$.gateway.error') AS VARCHAR) AS err,
  CAST(json_extract(attributes, '$.gateway.is_sse') AS VARCHAR) AS is_sse,
  count(DISTINCT CAST(json_extract(attributes, '$.gateway.exchange_id') AS VARCHAR)) AS exchanges
FROM ai_gateway_messages
WHERE date >= '2026-06-01'
  AND CAST(json_extract(attributes, '$.gateway.upstream') AS VARCHAR) LIKE '%anthropic%'
  AND json_extract(attributes, '$.gateway.error') IS NOT NULL
GROUP BY 1,2 ORDER BY exchanges DESC"

Code-path analysis

In hypaware-core/plugins-workspace/ai-gateway/src/proxy.js:

  1. Upstream error after headers sent → abrupt client destroy (proxy.js:175-188). If
    upstreamReq errors after res.headersSent, the gateway calls res.destroy(err) (proxy.js:180).
    The client sees exactly "socket connection was closed unexpectedly" with no status code to drive
    retry logic.

  2. No HTTP server timeout tuning (proxy.js:50-99). The listener uses Node defaults, including
    server.keepAliveTimeout = 5 s. Claude Code keeps a persistent connection to the local gateway; if
    a request lands just as the server closes an idle keep-alive socket, the client gets ECONNRESET.
    These races never produce an exchange row (no request is parsed), so the recorded error counts
    above are a lower bound — this may actually be the dominant cause given how frequently the error
    appears relative to recorded upstream errors.

  3. No keep-alive agent for upstream requests — each upstream connection relies on default agent
    behavior, increasing exposure to upstream resets on connection reuse.

Suggested fixes

  • Raise server.keepAliveTimeout (e.g. 65 s+) and server.headersTimeout accordingly on the
    gateway listener, or disable server-side idle closes for the loopback listener entirely.
  • On upstream error mid-SSE, consider emitting a terminal SSE error event (matching Anthropic's
    stream error shape) before ending the response, instead of res.destroy(err), so the client fails
    gracefully.
  • Use a keep-alive http.Agent/undici pool with sensible keepAliveTimeout for upstream requests,
    and optionally retry idempotent upstream connection setup failures before headers are sent.
  • Record a gateway-side counter/log for server-level clientError/socket-close events so keep-alive
    races become observable (they currently leave no trace).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions