nexu-io/open-design · warning · Error

${task.folder}: spritesheet is not webp/png/gif

Error message

${task.folder}: spritesheet is not webp/png/gif

What it means

Thrown by writePet when the downloaded binary passes the size check (>=16 bytes) but its magic bytes don't match webp (RIFF...WEBP), png (89 50 4E 47...), or gif (GIF87a/GIF89a) signatures. This is the HTML-error-page guard: CDNs sometimes return a 200 OK with an HTML error page, and without this check the UI would adopt a pet whose sprite is <!doctype html>. The comment at line 211 documents this exact scenario.

Source

Thrown at apps/daemon/src/community-pets-sync.ts:215

  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);
  if (bytes.length < 16) {
    throw new Error(`${task.folder}: spritesheet too small (${bytes.length} bytes)`);
  }
  // Reject HTML error pages dressed as `.webp` so the UI doesn't end up
  // adopting a pet whose sprite is `<!doctype html>`.
  const head = bytes.subarray(0, 12);
  const isWebp = head.toString('ascii', 0, 4) === 'RIFF' && head.toString('ascii', 8, 12) === 'WEBP';
  const isPng = head.toString('hex', 0, 8) === '89504e470d0a1a0a';
  const isGif = head.toString('ascii', 0, 6) === 'GIF87a' || head.toString('ascii', 0, 6) === 'GIF89a';
  if (!isWebp && !isPng && !isGif) {
    throw new Error(`${task.folder}: spritesheet is not webp/png/gif`);
  }
  await writeFile(sheetPath, bytes);
  await writeFile(manifestPath, JSON.stringify(task.manifest, null, 2) + '\n', 'utf8');
  return 'wrote';
}

async function runPool<T, R>(
  items: T[],
  concurrency: number,
  worker: (item: T, index: number) => Promise<R>,
): Promise<R[]> {
  const results: R[] = new Array(items.length);
  let cursor = 0;
  const workers = Array.from(
    { length: Math.min(concurrency, items.length) },
    async () => {
      for (;;) {
        const idx = cursor++;

View on GitHub (pinned to 5be4028344)

Solutions

  1. Open the spritesheetUrl directly to verify it serves a webp/png/gif image.
  2. If the host serves JPEG or another format, extend the format check or convert the asset.
  3. Check if the CDN is returning an HTML error page for a missing asset.
  4. Mark the pet as failed if the asset is permanently in an unsupported format.
Defensive patterns

Strategy: validation

Validate before calling

// After download, validate the magic bytes before writing.
const MAGIC = {
  webp: (b: Buffer) => b.toString('ascii', 0, 4) === 'RIFF' && b.toString('ascii', 8, 12) === 'WEBP',
  png: (b: Buffer) => b.toString('hex', 0, 8) === '89504e470d0a1a0a',
  gif: (b: Buffer) => {
    const sig = b.toString('ascii', 0, 6);
    return sig === 'GIF87a' || sig === 'GIF89a';
  },
};

function detectImageFormat(bytes: Buffer): 'webp' | 'png' | 'gif' | null {
  if (bytes.length < 12) return null;
  if (MAGIC.webp(bytes)) return 'webp';
  if (MAGIC.png(bytes)) return 'png';
  if (MAGIC.gif(bytes)) return 'gif';
  return null;
}

// Use to pre-validate or to diagnose
const fmt = detectImageFormat(bytes);
if (!fmt) {
  console.warn(`Unexpected content-type for ${url}: ${bytes.subarray(0, 32).toString('utf8')}`);
}

Type guard

export function isSpritesheetFormatError(error: unknown): boolean {
  return error instanceof Error && error.message.includes('spritesheet is not webp/png/gif');
}

Try / catch

try {
  await writePet(task, outRoot, force);
} catch (error) {
  if (error instanceof Error && error.message.includes('spritesheet is not webp/png/gif')) {
    // The CDN likely served an HTML error page — skip this pet
    syncResult.failed++;
    syncResult.errors.push(error.message);
    continue;
  } else throw error;
}

Prevention

When it happens

Trigger: downloadBinary returns a body that starts with <!doctype html> or some other non-image content, but is >=16 bytes so it passes the size check. The magic-byte inspection at lines 214-216 fails all three format checks, and the error fires at line 215.

Common situations: The spritesheet URL returns an HTML 404/error page with HTTP 200 (common with SPA CDNs). A CDN serves a generic placeholder image in a format the sync doesn't support (e.g., JPEG, AVIF). The hosting service wraps responses in HTML.

Related errors


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