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

  1. Verify you are authenticated as the user that created the thread (re-login clears stale sessions).
  2. Create a new thread under the current user and resubmit the run.
  3. 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.
  4. 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

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


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