From b7abb7ea016313543e6f2b340feb3969f9922511 Mon Sep 17 00:00:00 2001 From: claude_code Date: Tue, 23 Jun 2026 03:01:10 +0300 Subject: [PATCH] feat(ai-http): log detailed fetch error cause chain Node's fetch returns a generic "fetch failed" error, hiding the actual reason (e.g., ECONNRESET, timeout) in the error's cause chain. This change extracts up to three levels of the cause, formats each with its code and message, and includes the chain in the warning log, making failures more actionable. --- apps/server/src/integrations/ai/ai-http.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/server/src/integrations/ai/ai-http.ts b/apps/server/src/integrations/ai/ai-http.ts index b894ae9b..650c5f60 100644 --- a/apps/server/src/integrations/ai/ai-http.ts +++ b/apps/server/src/integrations/ai/ai-http.ts @@ -128,8 +128,21 @@ export const aiFetch: typeof fetch = async (input, init) => { return res; } catch (err) { const ms = Math.round(performance.now() - startedAt); + // Node's fetch reports a generic "fetch failed"; the real reason (e.g. an + // undici SocketError with .code ECONNRESET / UND_ERR_SOCKET / + // UND_ERR_*TIMEOUT) lives in err.cause (sometimes nested one level deeper). + // Surface the code+message of the cause chain so the failure is actionable. + const parts: string[] = []; + let cur: unknown = err; + for (let depth = 0; cur && depth < 3; depth++) { + const e = cur as { code?: string; message?: string; cause?: unknown }; + const code = e.code ? `[${e.code}] ` : ''; + const msg = e.message ?? String(e); + parts.push(`${code}${msg}`); + cur = e.cause; + } logger.warn( - `provider request #${id} x after ${ms}ms: ${(err as Error)?.message ?? String(err)}`, + `provider request #${id} x after ${ms}ms: ${parts.join(' <- ')}`, ); throw err; }