fix(ai): swallow image-search network failures into the existing fallback

`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.
This commit is contained in:
Fini 2026-04-26 07:30:00 +08:00
parent a727632a63
commit a5952bc88b

View file

@ -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<ImageSearchResult[]> {
@ -280,14 +292,21 @@ async function fetchFromWikimedia(query: string, count: number): Promise<ImageSe
url.searchParams.set('format', 'json');
url.searchParams.set('origin', '*');
const res = await fetch(url.toString());
if (!res.ok) return [];
// Same network-failure shielding as Openverse: an empty result is the
// documented fallback signal, returning [] keeps the endpoint at 200
// with placeholder-friendly results instead of bubbling the throw.
try {
const res = await fetch(url.toString(), { signal: AbortSignal.timeout(8000) });
if (!res.ok) return [];
const data = (await res.json()) as WikimediaQueryResponse;
const pages = data.query?.pages;
if (!pages) return [];
const data = (await res.json()) as WikimediaQueryResponse;
const pages = data.query?.pages;
if (!pages) return [];
return mapWikimediaPages(pages);
return mapWikimediaPages(pages);
} catch {
return [];
}
}
// ---------------------------------------------------------------------------