bytedance/deer-flow · error · HTTPException

Path is not a file: {skill_file_path}

Error message

Path is not a file: {skill_file_path}

What it means

HTTP 400 from _load_skill_archive_member: the .skill path exists but is not a regular file (actual_skill_path.is_file() is False) — typically a directory named like a skill archive. The ZIP open would fail confusingly, so it is rejected up front with a clear status.

Source

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

            # Not found
            return None
    except (zipfile.BadZipFile, KeyError):
        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}")

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Point the request at the packed .skill file, not the unpacked directory
  2. If only the directory form exists, re-zip it into a .skill archive first
  3. Filter non-file .skill entries out of preview UIs
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def is_skill_archive_file(p: str) -> bool:
    path = Path(p)
    return path.exists() and path.is_file()

Try / catch

if resp.status_code == 400 and "Path is not a file" in resp.text:
    offer_unpacked_tree_preview(path)  # directory form

Prevention

When it happens

Trigger: GET artifact for a path ending in .skill that is actually a directory (e.g. an unpacked skill folder saved to outputs with the same name), or a special file at that path.

Common situations: Agents unpacking skill packs into outputs and leaving a directory where the archive used to be, or unzipped leftovers from manual debugging.

Related errors


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