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:
-
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.
-
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.
-
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).
Symptom
When Claude Code is attached to the HypAware AI gateway (
ANTHROPIC_BASE_URLpointed at the localproxy), sessions intermittently show:
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
@hypaware/ai-gateway,@hypaware/claude,@hypaware/codex,@hypaware/local-fs,@hypaware/format-parquetclient_version)Evidence from local recordings
Distinct gateway exchanges with errors on the
anthropicupstream since 2026-06-01:client_abortedclient_abortedsocket hang upread ECONNRESETread ETIMEDOUTThe
read ECONNRESETcase is the smoking gun for the visible error: it happened on an SSE exchange(
/v1/messages?beta=true) withstatus_code: 200already 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 upcases occurred before headers were sent and were correctly converted to a 502JSON response (
response_bytes: 0), which Claude Code can retry — those are handled fine.Query used:
Code-path analysis
In
hypaware-core/plugins-workspace/ai-gateway/src/proxy.js:Upstream error after headers sent → abrupt client destroy (
proxy.js:175-188). IfupstreamReqerrors afterres.headersSent, the gateway callsres.destroy(err)(proxy.js:180).The client sees exactly "socket connection was closed unexpectedly" with no status code to drive
retry logic.
No HTTP server timeout tuning (
proxy.js:50-99). The listener uses Node defaults, includingserver.keepAliveTimeout= 5 s. Claude Code keeps a persistent connection to the local gateway; ifa 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.
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
server.keepAliveTimeout(e.g. 65 s+) andserver.headersTimeoutaccordingly on thegateway listener, or disable server-side idle closes for the loopback listener entirely.
errorevent (matching Anthropic'sstream error shape) before ending the response, instead of
res.destroy(err), so the client failsgracefully.
http.Agent/undici pool with sensiblekeepAliveTimeoutfor upstream requests,and optionally retry idempotent upstream connection setup failures before headers are sent.
clientError/socket-close events so keep-aliveraces become observable (they currently leave no trace).