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 messages

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. List the user's runs via the runs collection endpoint and confirm the run id still exists.
  2. Verify the request carries the same authentication as the one that created the run (ownership is enforced silently).
  3. 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

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


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