From b28b7def193da4aa84ef99c3f6ed158d1515c3d2 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sat, 21 Mar 2026 12:15:08 -0400 Subject: [PATCH] api: proper error messages for connection failures and HTTP errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Connection errors now show cause (refused/timeout/request error), URL, and the underlying error without redundant URL repetition - HTTP errors show status code, URL, and up to 1000 chars of body - Unparseable SSE events logged with content preview instead of silently dropped — may contain error info from vllm/server - Stream errors already had good context (kept as-is) You can't debug what you can't see. Co-Authored-By: Claude Opus 4.6 (1M context) --- poc-agent/src/api/mod.rs | 20 ++++++++++++++++---- poc-agent/src/api/openai.rs | 12 ++++++++++-- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/poc-agent/src/api/mod.rs b/poc-agent/src/api/mod.rs index 6c3079f..ef56701 100644 --- a/poc-agent/src/api/mod.rs +++ b/poc-agent/src/api/mod.rs @@ -138,7 +138,18 @@ pub(crate) async fn send_and_check( .json(body) .send() .await - .map_err(|e| anyhow::anyhow!("Failed to send request to API: {}", e))?; + .map_err(|e| { + let cause = if e.is_connect() { + "connection refused" + } else if e.is_timeout() { + "request timed out" + } else if e.is_request() { + "request error" + } else { + "unknown" + }; + anyhow::anyhow!("{} ({}): {}", cause, url, e.without_url()) + })?; let status = response.status(); let elapsed = start.elapsed(); @@ -164,12 +175,13 @@ pub(crate) async fn send_and_check( if !status.is_success() { let body = response.text().await.unwrap_or_default(); let _ = ui_tx.send(UiMessage::Debug(format!( - "API error {} after {:.1}s: {}", + "HTTP {} after {:.1}s ({}): {}", status, elapsed.as_secs_f64(), - &body[..body.len().min(300)] + url, + &body[..body.len().min(500)] ))); - anyhow::bail!("API error {}: {}", status, &body[..body.len().min(500)]); + anyhow::bail!("HTTP {} ({}): {}", status, url, &body[..body.len().min(1000)]); } if debug { diff --git a/poc-agent/src/api/openai.rs b/poc-agent/src/api/openai.rs index 7ad6b53..437c2e7 100644 --- a/poc-agent/src/api/openai.rs +++ b/poc-agent/src/api/openai.rs @@ -79,9 +79,17 @@ pub async fn stream( anyhow::bail!("API error in stream: {} {}", err_msg, raw); } - let chunk: ChatCompletionChunk = match serde_json::from_value(event) { + let chunk: ChatCompletionChunk = match serde_json::from_value(event.clone()) { Ok(c) => c, - Err(_) => continue, + Err(e) => { + // Log unparseable events — they may contain error info + let preview = event.to_string(); + let _ = ui_tx.send(UiMessage::Debug(format!( + "unparseable SSE event ({}): {}", + e, &preview[..preview.len().min(300)] + ))); + continue; + } }; if chunk.usage.is_some() {