nexu-io/open-design · warning · Error
download ${url} failed: ${resp.status} ${resp.statusText}
Error message
download ${url} failed: ${resp.status} ${resp.statusText} What it means
Thrown by downloadBinary when the HTTP GET to a spritesheet URL returns a non-2xx status. This function downloads the actual image binary for a pet spritesheet. The error includes the URL and HTTP status+statusText. Common causes: the spritesheet URL is a CDN link that has expired, the hosting service returned a 403/404, or a transient network error.
Source
Thrown at apps/daemon/src/community-pets-sync.ts:186
description: pet.description ?? '',
spritesheetPath: 'spritesheet.webp',
author: pet.authorLabel,
authorXUrl: pet.authorXUrl,
source: 'j20-hatchery',
sourceUrl: pet.galleryUrl,
},
spritesheetUrl: pet.spritesheetUrl,
spritesheetExt: extOf(pet.spritesheetUrl),
});
if (limit && tasks.length >= limit) break;
}
return tasks;
}
async function downloadBinary(url: string): Promise<Buffer> {
const resp = await fetch(url);
if (!resp.ok) {
throw new Error(`download ${url} failed: ${resp.status} ${resp.statusText}`);
}
const ab = await resp.arrayBuffer();
return Buffer.from(ab);
}
async function writePet(
task: PetTask,
outRoot: string,
force: boolean,
): Promise<'wrote' | 'skipped'> {
const dir = path.join(outRoot, task.folder);
const sheetPath = path.join(dir, `spritesheet.${task.spritesheetExt}`);
const manifestPath = path.join(dir, 'pet.json');
if (!force && (await pathExists(sheetPath)) && (await pathExists(manifestPath))) {
return 'skipped';
}
await mkdir(dir, { recursive: true });
const bytes = await downloadBinary(task.spritesheetUrl);View on GitHub (pinned to 5be4028344)
Solutions
- Retry the download — transient CDN errors often resolve on retry.
- Check if the spritesheet URL is still valid (not an expired presigned link).
- If the asset is permanently gone, the pet entry should be skipped or marked as failed in the sync result.
- Add a download timeout via AbortSignal to avoid hanging on unresponsive hosts.
Example fix
// before — bare fetch with no timeout
const resp = await fetch(url);
if (!resp.ok) throw new Error(`download ${url} failed: ${resp.status}`);
// after — timeout and retry
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15_000);
try {
const resp = await fetch(url, { signal: controller.signal });
if (!resp.ok) throw new Error(`download ${url} failed: ${resp.status} ${resp.statusText}`);
return Buffer.from(await resp.arrayBuffer());
} finally {
clearTimeout(timeout);
} Defensive patterns
Strategy: retry
Validate before calling
// Before downloading, validate the URL is well-formed and uses HTTPS.
function isValidSpritesheetUrl(url: string): boolean {
try {
const parsed = new URL(url);
return parsed.protocol === 'https:' && parsed.pathname.length > 1;
} catch {
return false;
}
}
// Check before calling downloadBinary
if (!isValidSpritesheetUrl(task.spritesheetUrl)) {
throw new Error(`Invalid spritesheet URL: ${task.spritesheetUrl}`);
} Type guard
export function isDownloadError(error: unknown): boolean {
return error instanceof Error && error.message.startsWith('download ') && error.message.includes('failed:');
} Try / catch
async function downloadWithRetry(url: string, maxAttempts = 3): Promise<Buffer> {
let lastError: unknown;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await downloadBinary(url);
} catch (error) {
lastError = error;
if (error instanceof Error && error.message.includes('failed:')) {
// HTTP error — retry with backoff for transient statuses
await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
continue;
}
throw error;
}
}
throw lastError;
} Prevention
- Add a download timeout (AbortSignal) to avoid hanging on unresponsive hosts.
- Retry transient HTTP errors (5xx, 429) with exponential backoff.
- Validate URLs are HTTPS before downloading.
- Surface per-pet download failures in the sync result rather than aborting the entire sync.
When it happens
Trigger: During writePet, downloadBinary(task.spritesheetUrl) is called. The fetch returns non-ok (e.g., 403 Forbidden from an expired presigned URL, 404 for a removed asset, or 5xx from the CDN). The error fires at line 188.
Common situations: A PetShare spritesheet URL uses a Supabase presigned URL that expired. A Hatchery sprite was deleted from its CDN. Rate limiting from the image host. A CDN misconfiguration returns 403 for hotlinked images.
Related errors
- petshare list page ${page} failed: ${resp.status} ${resp.sta
- hatchery list failed: ${resp.status} ${resp.statusText}
- ${task.folder}: spritesheet too small (${bytes.length} bytes
- openrouter video download ${dlResp.status}
- Could not fetch ${url} — the site may block server-side requ
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/6893b0f326b0ca1a.
Report an issue: GitHub.