nexu-io/open-design · error · Error

invalid zip local header: ${entry.name}

Error message

invalid zip local header: ${entry.name}

What it means

For each entry, readEntryBody reads the local file header at entry.localOffset (taken from the central directory). The first 4 bytes must be the local file header signature 0x04034b50 (LOCAL_SIG). A mismatch means the central directory's local-header offset is wrong or the local region is corrupt. Maps to HTTP 400.

Source

Thrown at apps/daemon/src/design/claude-design-import.ts:235

      isDirectory: name.endsWith('/'),
    });
    offset += 46 + nameLen + extraLen + commentLen;
  }
  return entries;
}

function findEndOfCentralDirectory(zip: Buffer): number {
  const min = Math.max(0, zip.length - 0xffff - 22);
  for (let i = zip.length - 22; i >= min; i -= 1) {
    if (zip.readUInt32LE(i) === EOCD_SIG) return i;
  }
  throw new Error('invalid zip: missing central directory');
}

function readEntryBody(zip: Buffer, entry: ZipEntry): Buffer {
  const offset = entry.localOffset;
  if (zip.readUInt32LE(offset) !== LOCAL_SIG) {
    throw new Error(`invalid zip local header: ${entry.name}`);
  }
  const nameLen = zip.readUInt16LE(offset + 26);
  const extraLen = zip.readUInt16LE(offset + 28);
  const bodyStart = offset + 30 + nameLen + extraLen;
  const bodyEnd = bodyStart + entry.compressedSize;
  if (bodyEnd > zip.length) throw new Error(`zip entry exceeds archive: ${entry.name}`);
  const compressed = zip.slice(bodyStart, bodyEnd);
  if (entry.method === 0) return Buffer.from(compressed);
  // A genuinely empty deflate payload would still occupy at least the BFINAL
  // marker; an entirely missing payload cannot be inflated, so treat it as
  // empty rather than handing a zero-length buffer to zlib.
  if (compressed.length === 0) return Buffer.alloc(0);
  // When the central directory advertises 0 (streaming zips with data
  // descriptors), fall back to the per-file ceiling so legitimate non-empty
  // payloads decode instead of being silently truncated. The post-decode
  // checks in the caller enforce MAX_FILE_BYTES and total-bytes limits.
  const cap = entry.uncompressedSize > 0 ? entry.uncompressedSize : MAX_FILE_BYTES;
  return inflateRawSync(compressed, { maxOutputLength: cap });

View on GitHub (pinned to 5be4028344)

Solutions

  1. Re-export fresh from Claude Design.
  2. Re-zip with a standard conformant tool (Info-ZIP, Finder, Windows Explorer).
  3. Avoid self-extracting wrappers; upload a plain deflate zip.
Defensive patterns

Strategy: try-catch

Validate before calling

// Local-header/offset drift isn't cheap to detect without a full parser;
// rely on `unzip -t` as the pre-flight.
import { execFileSync } from 'node:child_process';
function zipPassesTest(file: string): boolean {
  try { execFileSync('unzip', ['-tqq', file], { stdio: 'ignore' }); return true; }
  catch { return false; }
}

Try / catch

try {
  await importClaudeDesignZip(zipPath, projectDir);
} catch (err) {
  if (String(err).includes('invalid zip local header')) {
    return res.status(400).json({ error: 'zip has inconsistent local headers; re-create it' });
  }
  throw err;
}

Prevention

When it happens

Trigger: A zip where the central directory points at offsets that don't hold local headers — typically from a packager that wrote inconsistent offsets, a zip edited after creation, or bytes prepended (e.g. a self-extracting stub) that shifted local headers without adjusting offsets.

Common situations: Self-extracting archives with a prepended stub; zips modified by tools that rewrite the central directory without fixing offsets; archives repaired by 'zip -FF' that left offset drift.

Related errors


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