bytedance/deer-flow · error · HTTPException
Skill file not found: {skill_file_path}
Error message
Skill file not found: {skill_file_path} What it means
HTTP 404 from _load_skill_archive_member (worker thread of GET artifact): the .skill file itself does not exist at the resolved path (actual_skill_path.exists() is False). The archive-level check runs before any member lookup — the whole pack is gone, distinct from a missing internal member.
Source
Thrown at backend/app/gateway/routers/artifacts.py:277
return _read_skill_archive_member(zip_ref, info)
# 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.
"""View on GitHub (pinned to 1dd6ba1acb)
Solutions
- Re-list the thread's artifacts to see whether the .skill file still exists under its actual name
- If the pack should exist, re-run the step that produces/installs it into outputs
- Handle 404 by clearing the preview rather than retrying
Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def skill_archive_exists(actual_skill_path: str) -> bool:
return Path(actual_skill_path).exists() Try / catch
if resp.status_code == 404 and "Skill file not found" in resp.text:
clear_preview(); refresh_artifact_list() Prevention
- Refresh listings after runs that rewrite outputs before offering skill previews
- Distinguish this 404 (whole archive missing) from the internal-member 404 by detail text
- Avoid caching .skill artifact paths across runs
When it happens
Trigger: GET /api/threads/{thread_id}/artifacts for a .skill path (e.g. 'mnt/user-data/outputs/x.skill#SKILL.md') where the .skill file was deleted or never written to outputs.
Common situations: Previewing a skill pack referenced from stale thread state after the run regenerated outputs, deleted packs, or paths built from old listings.
Related errors
- File '{internal_path}' not found in skill archive
- Artifact not found: {path}
- Skill archives cannot be edited in the artifacts panel
- Skill archive member is too large to preview
- Path is not a file: {skill_file_path}
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/6cf81123e5a5a233.
Report an issue: GitHub.