claude-code - 💡(How to fix) Fix Bug: ECONNRESET ('terminated') during streaming with partial data kills the session — no retry, no fallback

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

TypeError: terminated at Fetch.onAborted (node:internal/deps/undici/undici:11132:53) at Fetch.terminate (node:internal/deps/undici/undici:10290:14)

Root Cause

Root cause (from source analysis)

Code Example

TypeError: terminated
    at Fetch.onAborted (node:internal/deps/undici/undici:11132:53)
    at Fetch.terminate (node:internal/deps/undici/undici:10290:14)

---

// Pseudocode from deobfuscated catch block:
let errorToThrow = streamIdleAborted
  ? Error(newMessages.length > 0 ? "partial response" : "no chunks")
  : originalError;  // ← TypeError("terminated")

if (newMessages.length > 0) throw errorToThrow;  // ← NO FALLBACK
// Only reaches non-streaming fallback if newMessages.length === 0

---

if (error.status === 429) return true;  // rate limit
if (error.status >= 500) return true;   // server error
return false;
// TypeError("terminated") has no .status → NOT retried

---

19:38:28 [DEBUG] [BackendRegistry] isInProcessEnabled: true
19:38:32 [ERROR] Error streaming, falling back to non-streaming mode: terminated
19:38:32 [DEBUG] [API:request] Creating client...  ← non-streaming retry succeeds

---

08:18:15 [WARN]  Streaming idle warning: no chunks received for 90s
08:18:46 [ERROR] Error in API request: terminated
08:18:46 [ERROR] Connection error details: code=ECONNRESET, message=read ECONNRESET
08:18:46 [ERROR] TypeError: terminated
           at Fetch.onAborted (undici:11132:53)
           at Fetch.terminate (undici:10290:14)

---

09:18:58 [WARN]  Streaming idle warning: no chunks received for 90s
09:19:28 [ERROR] Error in API request: terminated
09:19:28 [ERROR] Connection error details: code=ECONNRESET, message=read ECONNRESET

---

function shouldRetry(error) {
  if (error.status === 429 || error.status >= 500) return true;
  // Add: transient connection errors
  const msg = error.message || '';
  if (msg.includes('terminated') || msg.includes('ECONNRESET') ||
      msg.includes('ETIMEDOUT') || msg.includes('EPIPE')) return true;
  return false;
}
RAW_BUFFERClick to expand / collapse

Bug: ECONNRESET ("terminated") during streaming with partial data kills the session — no retry, no fallback

What's Wrong?

When the server resets the TCP connection mid-stream (ECONNRESET), Claude Code surfaces API Error: terminated and stops. No retry, no non-streaming fallback — the agent dies and the user must manually retry. This happens even though:

  1. The error is transient (ECONNRESET is a recoverable network condition)
  2. Non-streaming fallback exists and works for the no-data case
  3. The retry wrapper (withRetry) handles 429/5xx but not TypeError("terminated")

Root cause (from source analysis)

The terminated error is a TypeError thrown by undici's Fetch.terminate() when the server sends TCP RST:

TypeError: terminated
    at Fetch.onAborted (node:internal/deps/undici/undici:11132:53)
    at Fetch.terminate (node:internal/deps/undici/undici:10290:14)

This error reaches the streaming catch block in queryModel(), where two conditions prevent recovery:

Condition 1: newMessages.length > 0 throws without fallback

// Pseudocode from deobfuscated catch block:
let errorToThrow = streamIdleAborted
  ? Error(newMessages.length > 0 ? "partial response" : "no chunks")
  : originalError;  // ← TypeError("terminated")

if (newMessages.length > 0) throw errorToThrow;  // ← NO FALLBACK
// Only reaches non-streaming fallback if newMessages.length === 0

If any content_block_stop events were received before the RST, newMessages.length > 0 → the error is thrown directly → no non-streaming fallback. This is the same v2.1.104 regression that affects partial response received (#46987).

Condition 2: shouldRetry() doesn't match TypeError

The retry wrapper only retries errors with HTTP status codes:

if (error.status === 429) return true;  // rate limit
if (error.status >= 500) return true;   // server error
return false;
// TypeError("terminated") has no .status → NOT retried

Evidence from debug logs

Same session, same project, two different outcomes depending on whether partial data was received before the RST:

Case 1 — Apr 17, no partial data → FALLBACK WORKS:

19:38:28 [DEBUG] [BackendRegistry] isInProcessEnabled: true
19:38:32 [ERROR] Error streaming, falling back to non-streaming mode: terminated
19:38:32 [DEBUG] [API:request] Creating client...  ← non-streaming retry succeeds

Case 2 — Apr 20, partial data received → NO RECOVERY:

08:18:15 [WARN]  Streaming idle warning: no chunks received for 90s
08:18:46 [ERROR] Error in API request: terminated
08:18:46 [ERROR] Connection error details: code=ECONNRESET, message=read ECONNRESET
08:18:46 [ERROR] TypeError: terminated
           at Fetch.onAborted (undici:11132:53)
           at Fetch.terminate (undici:10290:14)

No fallback attempted. Agent stopped. User had to manually retry.

Same pattern repeated 30 minutes later:

09:18:58 [WARN]  Streaming idle warning: no chunks received for 90s
09:19:28 [ERROR] Error in API request: terminated
09:19:28 [ERROR] Connection error details: code=ECONNRESET, message=read ECONNRESET

Again no recovery.

Environment

  • Claude Code: v2.1.112 (npm)
  • Model: claude-opus-4-6 (1M context)
  • Plan: Max ($200/month)
  • OS: Windows 10, Git Bash
  • CLAUDE_ENABLE_STREAM_WATCHDOG=1
  • CLAUDE_STREAM_IDLE_TIMEOUT_MS=180000

What Should Happen?

  1. ECONNRESET should be retryable. shouldRetry() should match TypeError("terminated") (and other connection errors like ETIMEDOUT, EPIPE, EHOSTUNREACH) the same way it matches 5xx. These are transient network conditions, not permanent failures.

  2. Non-streaming fallback should work regardless of partial data. The if (newMessages.length > 0) throw guard (introduced in v2.1.104) prevents fallback for the most common real-world failure: model starts responding, then the connection drops. This is the exact scenario where fallback is most valuable — the user's prompt was valid, the model was working, only the transport failed.

  3. At minimum, the agent should retry the same request instead of surfacing a raw TypeError("terminated") to the user and stopping. Even without non-streaming fallback, a simple streaming retry after a 2-3s backoff would recover from most ECONNRESET events.

Suggested fix

Option A — Add connection errors to shouldRetry():

function shouldRetry(error) {
  if (error.status === 429 || error.status >= 500) return true;
  // Add: transient connection errors
  const msg = error.message || '';
  if (msg.includes('terminated') || msg.includes('ECONNRESET') ||
      msg.includes('ETIMEDOUT') || msg.includes('EPIPE')) return true;
  return false;
}

Option B — Remove the newMessages.length > 0 throw guard: Allow non-streaming fallback even when partial data was received. Yes, this means the same prompt runs twice (double token cost on that turn). But it's strictly better than killing the session and losing all context.

Option C — Both A and B (recommended): retry first (handles transient RST), fall back to non-streaming if retries exhausted (handles persistent streaming issues).

Related issues

  • #46987 — Stream idle timeout - partial response received (same newMessages.length > 0 throw, 100 comments, 86 👍)
  • #33949 — SSE streaming hangs root cause analysis (33 comments, 21 👍)
  • #39755 — Watchdog fallback dead code
  • anthropic-sdk-typescript#998 — Ping-aware streaming watchdog proposal (architectural fix for the family of streaming reliability bugs)

Is this a regression?

Partially. The newMessages.length > 0 throw was introduced in v2.1.104. Before that, ECONNRESET with partial data would silently fall back to non-streaming mode (double token cost but session survived). v2.1.111 reverted a related retry cap from v2.1.110, but didn't address this specific throw condition.

extent analysis

TL;DR

The most likely fix for the ECONNRESET issue during streaming with partial data is to modify the shouldRetry() function to include connection errors and remove the newMessages.length > 0 throw guard to allow non-streaming fallback.

Guidance

  • Modify the shouldRetry() function to include connection errors like ECONNRESET, ETIMEDOUT, EPIPE, and EHOSTUNREACH to enable retries for these transient network conditions.
  • Remove the newMessages.length > 0 throw guard to allow non-streaming fallback even when partial data was received, ensuring the session survives and context is preserved.
  • Consider implementing a retry mechanism with a backoff strategy to handle transient connection errors.
  • Review related issues, such as #46987 and #33949, to ensure a comprehensive solution that addresses the root cause of streaming reliability bugs.

Example

function shouldRetry(error) {
  if (error.status === 429 || error.status >= 500) return true;
  const msg = error.message || '';
  if (msg.includes('terminated') || msg.includes('ECONNRESET') ||
      msg.includes('ETIMEDOUT') || msg.includes('EPIPE')) return true;
  return false;
}

Notes

The proposed fix assumes that the shouldRetry() function is the primary mechanism for handling retries. Additional modifications may be necessary to ensure seamless integration with the existing retry wrapper and non-streaming fallback logic.

Recommendation

Apply workaround: Modify the shouldRetry() function and remove the newMessages.length > 0 throw guard to enable retries and non-streaming fallback for connection errors. This approach addresses the immediate issue and provides a foundation for further improvements to streaming reliability.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix Bug: ECONNRESET ('terminated') during streaming with partial data kills the session — no retry, no fallback