bytedance/deer-flow · info · HTTPException
Run {run_id} not found
Error message
Run {run_id} not found What it means
Raised as HTTP 404 from _resolve_run by every run-scoped read endpoint (e.g. GET /api/runs/{run_id}/messages) when run_store.get(run_id) returns None. The lookup is filtered by the requesting user's contextvar, so a run owned by someone else is indistinguishable from a nonexistent run.
Source
Thrown at backend/app/gateway/routers/runs.py:102
if snapshot_config.get("configurable", {}).get("checkpoint_id"):
return serialize_channel_values_for_api(snapshot.values)
except Exception:
logger.exception("Failed to fetch final state for run %s", record.run_id)
return {"status": record.status.value, "error": record.error}
# ---------------------------------------------------------------------------
# Run-scoped read endpoints
# ---------------------------------------------------------------------------
async def _resolve_run(run_id: str, request: Request) -> dict:
"""Fetch run by run_id with user ownership check. Raises 404 if not found."""
run_store = get_run_store(request)
record = await run_store.get(run_id) # user_id=AUTO filters by contextvar
if record is None:
raise HTTPException(status_code=404, detail=f"Run {run_id} not found")
return record
@router.get("/{run_id}/messages")
@require_permission("runs", "read")
async def run_messages(
run_id: str,
request: Request,
limit: int = Query(default=50, le=200, ge=1),
before_seq: int | None = Query(default=None, ge=1),
after_seq: int | None = Query(default=None, ge=1),
) -> dict:
"""Return paginated messages for a run (cursor-based).
Pagination:
- after_seq: messages with seq > after_seq (forward)
- before_seq: messages with seq < before_seq (backward)
- neither: latest messagesView on GitHub (pinned to 1dd6ba1acb)
Solutions
- List the user's runs via the runs collection endpoint and confirm the run id still exists.
- Verify the request carries the same authentication as the one that created the run (ownership is enforced silently).
- If runs are pruned by retention, re-initiate the run instead of polling the old id.
Example fix
// before
const res = await fetch(`/api/runs/${runId}/messages`);
if (!res.ok) throw new Error('failed');
// after
const res = await fetch(`/api/runs/${runId}/messages`);
if (res.status === 404) { stopPolling(); return; } Defensive patterns
Strategy: try-catch
Validate before calling
runs = requests.get(f"{BASE}/api/runs", headers=auth).json()
run_ids = {r["id"] for r in runs.get("runs", runs if isinstance(runs, list) else [])}
if run_id not in run_ids:
stop_polling(run_id) Type guard
def is_existing_run(rid: str, owned: set[str]) -> bool:
return rid in owned Try / catch
resp = requests.get(f"{BASE}/api/runs/{run_id}/messages", headers=auth)
if resp.status_code == 404:
stop_polling(run_id) # gone or not ours — terminal state
else:
resp.raise_for_status() Prevention
- Stop polling on the first 404; it never transitions back.
- Send the same auth identity that created the run.
- Persist run ids together with the auth principal that produced them.
When it happens
Trigger: GET /api/runs/{run_id}/messages (or sibling run endpoints) with a deleted run id, a typo'd id, or a valid id created under a different authenticated user.
Common situations: Polling a run after it was pruned/expired from the run store; sharing run ids between accounts; frontend retaining a stale run id after thread deletion.
Related errors
- Thread {thread_id} not found
- Thread not found
- Scheduled task not found
- Checkpoint {checkpoint_id} not found
- Failed to load workspace changes.
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/dd72879e09b6e21a.
Report an issue: GitHub.