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
- Point the request at the packed .skill file, not the unpacked directory
- If only the directory form exists, re-zip it into a .skill archive first
- 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
- Keep packed .skill archives and unpacked directories under distinct names in outputs
- Client-side is_file checks before preview avoid the round trip
- Note is_file() here follows symlinks, unlike the editable path's lstat check
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
- Only files in /mnt/user-data/outputs can be edited
- Path is not a file: {path}
- Skill archives cannot be edited in the artifacts panel
- Skill archive member is too large to preview
- Skill file not found: {skill_file_path}
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/c5ac8766a97a14ca.
Report an issue: GitHub.