From b9daac8a224a520fd8e0b04c3d461b301f6c47dd Mon Sep 17 00:00:00 2001 From: Fini Date: Sun, 26 Apr 2026 06:29:54 +0800 Subject: [PATCH] fix(ai): swallow image-search network failures into the existing fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `fetchFromOpenverse` and `fetchFromWikimedia` were missing try/catch, so a ConnectTimeoutError on `api.openverse.org` (frequent on networks that can't reach Openverse) bubbled up to nitro's default handler and turned a single image-search lookup into a HTTP 500 for the whole design generation flow. The handler already treats `null` (Openverse) and `[]` (Wikimedia) as the documented fallback signals — wrap the fetches and return those on any throw, plus an explicit 8s AbortSignal.timeout so the wait is bounded. --- apps/web/server/api/ai/image-search.ts | 51 ++++++++++++++++++-------- 1 file changed, 35 insertions(+), 16 deletions(-) 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