Hmbown/CodeWhale · error · anyhow::Error
err.last_error
Error message
err.last_error
What it means
After exhausting the bounded transport retry policy, the client rethrows the underlying error via anyhow::Error::new(err.last_error). The final error a caller sees is the last LlmError from the request loop, with its structured type preserved for downcasting so auth/rate-limit/invalid-request failures stay classifiable (#3884).
Solutions
- Downcast the anyhow error to LlmError to classify auth vs rate-limit vs network before deciding on user messaging
- Check the provider status / API key validity indicated by the underlying LlmError variant
- If transient, wait for the recovery probe to restore the connection and retry
Example fix
// before
if let Err(e) = client.send(req).await { log::error!("{}", e); }
// after
if let Err(e) = client.send(req).await {
if let Some(llm) = e.downcast_ref::<LlmError>() {
match llm { LlmError::Auth(_) => reauth(), LlmError::RateLimit(_) => backoff(), _ => {} }
}
} Defensive patterns
Strategy: try-catch
Type guard
fn classify(e: &anyhow::Error) -> Option<&LlmError> { e.downcast_ref::<LlmError>() } Try / catch
match client.send(req).await {
Err(e) => match e.downcast_ref::<LlmError>() {
Some(LlmError::Auth(_)) => reauthenticate(),
Some(LlmError::RateLimit(_)) => schedule_backoff(),
_ => report_transport_failure(&e),
},
ok => ok,
} Prevention
- Always downcast to LlmError instead of string-matching messages
- Monitor provider status pages and key validity for recurring failures
- Keep retry budgets sized to transient-only failures
When it happens
Trigger: Any LLM request that fails on every retry attempt: mark_request_failure has been called and maybe_probe_recovery has run, then the last_error is surfaced to the caller.
Common situations: Provider outage lasting longer than the retry window; persistent 401/403 from a revoked API key; sustained rate limiting; malformed requests rejected on every attempt.
Understand the failure class
Background: "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause — this error's family across 29 libraries.
Related errors
- Antigravity cloud-code is stream-only; blocking…
- DS4 /v1/models could not be reached at
- failed to fetch from
- Failed to list models: HTTP
- Model stream ended with no answer or tool call.
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/6f69664c66651ec9.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/client.rs:3506
Err(err) => {
if let LlmError::RateLimited { retry_after, .. } = &err.last_error {
crate::retry_status::note_rate_limit(
retry_after
.unwrap_or_else(|| retry_cfg.delay_for_attempt(retry_cfg.max_retries)),
);
}
let last = err.last_error.to_string();
if err.attempts > 1 {
crate::retry_status::failed(last.clone());
} else {
crate::retry_status::clear();
}
self.mark_request_failure(&last).await;
self.maybe_probe_recovery().await;
// Keep the structured `LlmError` downcastable so failure
// surfaces can classify auth/rate-limit/invalid-request
// instead of reporting an opaque string (#3884).
Err(anyhow::Error::new(err.last_error))
}
}
}
/// The same bounded transport retry policy without process-global retry
/// banners, provider-wide pause cells, or shared connection-health writes.
/// Used only by the Auto classifier during read-only request inspection.
async fn send_with_isolated_retry<F>(
&self,
mut build: F,
include_error_body: bool,
) -> Result<reqwest::Response>
where
F: FnMut() -> reqwest::RequestBuilder,
{
let retry_cfg: LlmRetryConfig = self.retry.clone().into();
let request_result = with_retry(
&retry_cfg,View on GitHub (pinned to 433685b202)