zylon-ai/private-gpt · error · SkillDomainError

INVALID_ZIP

INVALID_ZIP

Error message

Invalid ZIP file: {upload.filename or 'unnamed.zip'}

What it means

SkillDomainError with code INVALID_ZIP: any non-SkillDomainError exception raised while opening the archive with zipfile.ZipFile or reading its members is wrapped into this error, naming the upload's filename (or 'unnamed.zip'). Typical underlying causes are BadZipFile, truncated payloads, or read failures on individual members.

Source

Thrown at private_gpt/server/skills/skills_files.py:178


def _extract_zip(upload: UploadFile, payload: bytes) -> list[tuple[str, bytes]]:
    try:
        with zipfile.ZipFile(io.BytesIO(payload)) as archive:
            members = [item for item in archive.infolist() if not item.is_dir()]
            if not members or all(item.file_size == 0 for item in members):
                raise SkillDomainError(
                    SkillErrorCode.EMPTY_ZIP, "ZIP file cannot be empty"
                )

            raw_entries = [
                (item.filename, archive.read(item.filename)) for item in members
            ]
            return _flatten_wrapper_directory(raw_entries)
    except SkillDomainError as e:
        raise e
    except Exception as exc:
        raise SkillDomainError(
            SkillErrorCode.INVALID_ZIP,
            f"Invalid ZIP file: {upload.filename or 'unnamed.zip'}",
        ) from exc


def _flatten_wrapper_directory(
    entries: list[tuple[str, bytes]],
) -> list[tuple[str, bytes]]:
    """Find SKILL.md and strip wrapper directories above it.

    Per the Agent Skills spec, SKILL.md defines the skill root.
    This finds SKILL.md, determines its parent directory, and keeps only
    files at or below that level, stripping any wrapper directories above.

    Examples:
      - repo-name/SKILL.md -> SKILL.md
      - repo-name/skill-dir/SKILL.md -> SKILL.md (with sibling dirs excluded)
    """

View on GitHub (pinned to 4a030776a3)

Solutions

  1. Verify integrity locally: `unzip -t file.zip` (or `python -m zipfile -t file.zip`) — it must report 'No errors detected'.
  2. Confirm the actual format with `file file.zip`; re-export as a genuine ZIP if it is tar/gz/7z.
  3. Re-download/re-transfer the archive and compare sizes/checksums with the source.
  4. Remove password protection before upload.

Example fix

# before
# artifact.zip is actually a gzip stream renamed
# after
import gzip, zipfile
with gzip.open('artifact.zip', 'rb') as f:
    payload = f.read()
with zipfile.ZipFile('fixed.zip', 'w') as zf:
    zf.writestr('SKILL.md', payload.decode())  # upload fixed.zip
Defensive patterns

Strategy: validation

Validate before calling

import zipfile

def is_valid_zip(path: str) -> bool:
    try:
        with zipfile.ZipFile(path) as zf:
            return zf.testzip() is None
    except zipfile.BadZipFile:
        return False

Type guard

const looksLikeZip = (bytes: Uint8Array): boolean =>
  bytes.length >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b && (bytes[2] === 3 || bytes[2] === 5 || bytes[2] === 7);

Try / catch

try {
  await uploadSkill(zipBytes);
} catch (e: any) {
  if (e?.code === 'INVALID_ZIP') throw new Error('Archive failed integrity check — re-download or re-export');
  throw e;
}

Prevention

When it happens

Trigger: Uploading a file with a .zip extension or 'application/zip' content-type that is not a real zip (HTML error page saved as .zip, gzip/tar masquerading, text); a zip truncated in transit (client-side size cap, proxy cut); a corrupted or encrypted archive; archive.read() failing on a member with a bad CRC.

Common situations: Downloading artifacts from a URL that returned a login page instead of the zip; incomplete multipart uploads; double-compressed or wrong-format files renamed to .zip; CI caches serving corrupted archives; password-protected zips from shared drives.

Related errors


AI-assisted analysis of zylon-ai/private-gpt@4a030776a3 (2026-08-15). Data as JSON: /api/errors/6cbd002e1f60dd6e. Report an issue: GitHub.