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
- Open the spritesheetUrl directly to verify it serves a webp/png/gif image.
- If the host serves JPEG or another format, extend the format check or convert the asset.
- Check if the CDN is returning an HTML error page for a missing asset.
- 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
- Open the spritesheet URL in a browser to verify it serves a real image.
- If the host serves JPEG or AVIF, extend the format check or pre-convert.
- Treat HTML responses (CDN error pages) as broken assets, not transient failures.
- Log the first few bytes of unexpected payloads to diagnose CDN behavior.
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
- ${task.folder}: spritesheet too small (${bytes.length} bytes
- comment note required
- zip entry size mismatch: ${relPath}
- invalid zip central directory
- FIGMA_IMPORT_FAILED
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/dcb7c3bc50e5c870.
Report an issue: GitHub.