diff --git a/apps/web/server/api/ai/image-search.ts b/apps/web/server/api/ai/image-search.ts index b841d9230..c33de8bb2 100644 --- a/apps/web/server/api/ai/image-search.ts +++ b/apps/web/server/api/ai/image-search.ts @@ -254,17 +254,29 @@ async function fetchFromOpenverse( } } - const res = await fetch(url.toString(), { headers }); - if (res.status === 429) { - // Rate limited — signal fallback - return null; - } - if (!res.ok) { - return null; - } + // Network failures (ConnectTimeoutError on restricted networks, DNS + // failures, etc.) need to behave like a 429: return null so the caller + // falls back to Wikimedia. Without this, fetch() throws, the throw + // bubbles up to nitro's default handler, and the user sees a 500 + // instead of placeholder images. + try { + const res = await fetch(url.toString(), { + headers, + signal: AbortSignal.timeout(8000), + }); + if (res.status === 429) { + // Rate limited — signal fallback + return null; + } + if (!res.ok) { + return null; + } - const data = (await res.json()) as OpenverseSearchResponse; - return (data.results ?? []).map(mapOpenverseResult); + const data = (await res.json()) as OpenverseSearchResponse; + return (data.results ?? []).map(mapOpenverseResult); + } catch { + return null; + } } async function fetchFromWikimedia(query: string, count: number): Promise { @@ -280,14 +292,21 @@ async function fetchFromWikimedia(query: string, count: number): Promise