Hmbown/CodeWhale · error
SSE stream idle timeout after {}s — no data received (bytes_
Error message
SSE stream idle timeout after {}s — no data received (bytes_received={}, stream_age_ms={}, ms_since_last_chunk={}) What it means
The tokio timeout wrapping `byte_stream.next()` elapsed with no chunk received inside the idle window, so the client yields this error with diagnostics: idle seconds, total bytes received, stream age, and time since the last chunk. These fields distinguish a stream that never produced a byte (server never started) from one that stalled mid-generation.
Source
Thrown at crates/tui/src/client/chat.rs:1301
// Telemetry for #103 stream-decode diagnostics: bytes received
// since the start of this stream and last successful event time.
// Surfaces in the error log when reqwest yields a chunk error so
// we can tell HTTP/2 RST_STREAM from chunk-decode-failure from
// gzip-corruption when investigating a flaky session.
let stream_start = std::time::Instant::now();
let mut last_event_at = std::time::Instant::now();
let mut bytes_received: usize = 0;
// Set when a `[DONE]` sentinel was seen, so the post-loop flush does
// not re-process trailing post-DONE bytes.
let mut saw_done = false;
'stream: loop {
let chunk_result = match tokio_timeout(idle, byte_stream.next()).await {
Ok(Some(result)) => result,
Ok(None) => break, // Stream ended normally
Err(_elapsed) => {
yield Err(anyhow::anyhow!(stream_idle_timeout_message(
idle,
bytes_received,
stream_start.elapsed(),
last_event_at.elapsed(),
)));
break;
}
};
let chunk = match chunk_result {
Ok(bytes) => bytes,
Err(e) => {
// Walk the error source chain so reqwest's underlying
// hyper / h2 / io error is visible — without this the
// outer "error decoding response body" message tells
// us nothing about WHY the stream died.
let mut error_chain = format!("{e}");
let mut current: Option<&(dyn std::error::Error + 'static)> =
std::error::Error::source(&e);View on GitHub (pinned to 8880682c63)
Solutions
- Retry the request — a silent stall is usually transient
- Increase the stream idle timeout so it exceeds the model's expected time-to-first-token (reasoning models can think for tens of seconds silently)
- If bytes_received=0 consistently, the server is not starting the response — verify the endpoint and request
- Check for intermediaries (proxies, VPNs) that kill idle connections and bypass them
Defensive patterns
Strategy: retry
Type guard
fn is_stream_idle_timeout(err: &anyhow::Error) -> bool {
err.to_string().starts_with("SSE stream idle timeout")
} Try / catch
match stream.next().await {
Some(Err(e)) if is_stream_idle_timeout(&e) => {
backoff().await;
stream = client.reopen_stream(request).await?; // idempotent re-request
}
Some(Err(e)) => return Err(e),
other => { /* forward */ }
} Prevention
- Set the stream idle timeout above the model's worst-case time-to-first-token (reasoning models think silently for a long time)
- Use the diagnostics: bytes_received=0 means never-started, >0 with large ms_since_last_chunk means mid-stream stall
- Keep-alive or bypass intermediaries that idle-kill quiet SSE connections
When it happens
Trigger: Reasoning models that think for a long time before the first token, exceeding the idle window; a stalled proxy or network path; provider-side hang after 200 OK; idle timeout configured smaller than the provider's time-to-first-token.
Common situations: High-latency or mobile networks; deep-reasoning requests with long silent thinking phases; corporate proxies that idle-kill quiet connections.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- SSE stream idle timeout after {}s — no data received (bytes_
- Stream read error: {e}
- SSE stream idle timeout after {}s — no data received (bytes_
- SSE stream idle timeout after {}s — no data received (bytes_
- Runtime API request failed (${status}): ${message}
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/d0358acdf54368a6.
Report an issue: GitHub.