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

  1. Retry the download — transient CDN errors often resolve on retry.
  2. Check if the spritesheet URL is still valid (not an expired presigned link).
  3. If the asset is permanently gone, the pet entry should be skipped or marked as failed in the sync result.
  4. 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

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


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/6893b0f326b0ca1a. Report an issue: GitHub.