bytedance/deer-flow · error · HTTPException

File '{internal_path}' not found in skill archive

Error message

File '{internal_path}' not found in skill archive

What it means

HTTP 404 from _load_skill_archive_member: the .skill ZIP opened fine but _extract_file_from_skill_archive found no member matching the requested internal_path (returned None). The archive exists; the file inside it (e.g. 'SKILL.md' or a nested asset) does not.

Source

Thrown at backend/app/gateway/routers/artifacts.py:282

        return None


def _load_skill_archive_member(actual_skill_path: Path, skill_file_path: str, internal_path: str) -> tuple[bytes, str | None]:
    """Worker-thread body for the ``.skill`` branch of ``get_artifact``.

    The ``exists`` / ``is_file`` probes, the ZIP open+extract, and the MIME
    sniff (``mimetypes`` lazily stats the system MIME database on first use) are
    blocking filesystem IO and must stay off the event loop. Raised
    ``HTTPException``s propagate through ``asyncio.to_thread`` unchanged,
    preserving status codes.
    """
    if not actual_skill_path.exists():
        raise HTTPException(status_code=404, detail=f"Skill file not found: {skill_file_path}")
    if not actual_skill_path.is_file():
        raise HTTPException(status_code=400, detail=f"Path is not a file: {skill_file_path}")
    content = _extract_file_from_skill_archive(actual_skill_path, internal_path)
    if content is None:
        raise HTTPException(status_code=404, detail=f"File '{internal_path}' not found in skill archive")
    mime_type, _ = mimetypes.guess_type(internal_path)
    return content, mime_type


def _read_artifact_payload(actual_path: Path, path: str, download: bool) -> tuple[str, str | None]:
    """Worker-thread body for the regular branch of ``get_artifact``.

    Stat probes and MIME sniffing (``mimetypes`` lazily stats the system MIME
    database on first use) are blocking filesystem IO. Returns a
    ``(kind, mime_type)`` plan the handler turns into a streamed
    ``FileResponse``. Inline text and binary previews both use FileResponse so
    clients can request a bounded byte range instead of buffering a whole file.
    """
    if not actual_path.exists():
        raise HTTPException(status_code=404, detail=f"Artifact not found: {path}")
    if not actual_path.is_file():
        raise HTTPException(status_code=400, detail=f"Path is not a file: {path}")
    mime_type, _ = mimetypes.guess_type(actual_path)

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. List the archive's members (python -m zipfile -l pack.skill) and use the exact name, including any top-level folder
  2. Rebuild the archive so the expected file sits at the expected internal path
  3. Make pack structure conform to the skill spec so standard members like SKILL.md always exist

Example fix

# before
internal = "SKILL.md"           # pack actually has 'my-skill/SKILL.md' → 404

# after
internal = "my-skill/SKILL.md"  # exact member path from zipfile -l
Defensive patterns

Strategy: validation

Validate before calling

import zipfile

def archive_has_member(zip_path: str, internal_path: str) -> bool:
    with zipfile.ZipFile(zip_path) as z:
        names = set(z.namelist())
    return internal_path in names

Try / catch

if resp.status_code == 404 and "not found in skill archive" in resp.text:
    members = list_zip_members(zip_path)   # show what actually exists
    offer_member_picker(members)

Prevention

When it happens

Trigger: GET artifact with a .skill#internal-path where internal_path has wrong casing, a missing parent folder inside the archive, or references a member the pack simply does not ship (README vs SKILL.md confusion).

Common situations: UIs hardcoding 'SKILL.md' for packs that ship different layouts, hand-built archives with unexpected roots (extra top-level folder), or version changes in skill-pack structure.

Related errors


AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14). Data as JSON: /api/errors/0abd69cdcd96c97c. Report an issue: GitHub.