bytedance/deer-flow · error · HTTPException
Thread {thread_id} not found
Error message
Thread {thread_id} not found What it means
HTTP 404 raised during run submission when the authenticated user fails thread_store.check_access(thread_id, user.id). It is deliberately a 404 (not 403) so existence of another user's thread is not leaked. Internal system-role callers additionally get a second check against the trusted X-DeerFlow-Owner-User-Id header; missing rows and NULL-owner rows stay accessible, only threads owned by a different user are rejected.
Source
Thrown at backend/app/gateway/services.py:1124
# before any run is created, so one user cannot start runs on (or read /wait
# checkpoint state from) another user's thread. Missing rows (auto-created
# temp threads) and NULL-owner rows (shared / pre-auth data) stay accessible
# via check_access; only a thread already owned by another user is rejected
# with 404, matching thread_runs.py's anti-enumeration behaviour. Internal
# channel runs act on behalf of the connection owner carried in
# X-DeerFlow-Owner-User-Id, so they are scoped to that owner instead of
# bypassing the check -- a leaked internal token must not grant cross-user
# thread access.
user = getattr(request.state, "user", None)
if user is not None:
allowed = await run_ctx.thread_store.check_access(thread_id, str(user.id))
if not allowed and owner_user_id and getattr(user, "system_role", None) == INTERNAL_SYSTEM_ROLE:
# Channel workers may also act for the connection owner named in
# the trusted header (e.g. claiming a legacy default-owned channel
# thread for its real owner).
allowed = await run_ctx.thread_store.check_access(thread_id, owner_user_id)
if not allowed:
raise HTTPException(status_code=404, detail=f"Thread {thread_id} not found")
owner_context_token = set_current_user(SimpleNamespace(id=owner_user_id)) if owner_user_id else None
try:
agent_factory = resolve_agent_factory(body.assistant_id)
is_internal_caller = getattr(getattr(request, "state", None), "auth_source", None) == AUTH_SOURCE_INTERNAL
command = getattr(body, "command", None)
if command and command.get("resume") is not None:
graph_input = Command(resume=command["resume"])
else:
graph_input = normalize_input(body.input, trusted_internal=is_internal_caller)
config = build_run_config(thread_id, body.config, body.metadata, assistant_id=body.assistant_id)
await apply_checkpoint_to_run_config(config, body=body, thread_id=thread_id, request=request)
# Merge DeerFlow-specific context overrides into both ``configurable`` and ``context``.
# The ``context`` field is a custom extension for the langgraph-compat layer
# that carries agent configuration (model_name, thinking_enabled, etc.).
# Only agent-relevant keys are forwarded; unknown keys (e.g. thread_id) are ignored.
merge_run_context_overrides(config, getattr(body, "context", None), internal=is_internal_caller)View on GitHub (pinned to 1dd6ba1acb)
Solutions
- Verify you are authenticated as the user that created the thread (re-login clears stale sessions).
- Create a new thread under the current user and resubmit the run.
- If you are an integration/channel worker, confirm the X-DeerFlow-Owner-User-Id header names the thread's actual owner and the caller has the internal system role.
- An admin may transfer or clear thread ownership in the thread store if the thread genuinely belongs to this user.
Example fix
// before
await api.createRun('thread-from-another-user', body); // 404
// after
const { thread_id } = await api.createThread();
await api.createRun(thread_id, body); Defensive patterns
Strategy: validation
Validate before calling
// Only submit runs to threads the current user created
const mine = await api.listThreads();
const owned = new Set(mine.filter(t => t.owner_user_id === currentUser.id || t.owner_user_id == null).map(t => t.thread_id));
if (!owned.has(threadId)) throw new Error('thread not accessible for this user'); Try / catch
try { await api.createRun(threadId, body); } catch (e) { if (e.status === 404) { // treat as gone/inaccessible: create a fresh thread and restart the conversation; never retry same id } throw e; } Prevention
- Clear cached thread lists on logout/login transitions.
- Scope thread ids per-user in client storage (key by user id).
- Remember 404 here means 'not yours OR not exists' by design — don't distinguish, don't probe.
When it happens
Trigger: Starting (or resuming/waiting on) a run on a thread_id already owned by a different user, with a valid session for user A and a thread created by user B. Also hit when a leaked internal token presents an owner header that does not match the thread's real owner.
Common situations: Copy-pasting a thread URL/id between accounts; shared demo accounts; frontend caching a thread list from a previous login; IM channel workers acting on a legacy thread before ownership is claimed via the trusted-header path.
Related errors
- Model {model_name!r} is not in the configured model allowlis
- Failed to load MCP configuration
- Failed to update MCP configuration
- Failed to update MCP server
- HTTP ${response.status}: ${response.statusText}
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/0a14a1fe21174477.
Report an issue: GitHub.