{"record":{"id":"0a14a1fe21174477","repo":"bytedance/deer-flow","slug":"thread-thread-id-not-found-0a14a1","errorCode":null,"errorMessage":"Thread {thread_id} not found","messagePattern":"Thread (.+?) not found","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"error","filePath":"backend/app/gateway/services.py","lineNumber":1124,"sourceCode":"    # before any run is created, so one user cannot start runs on (or read /wait\n    # checkpoint state from) another user's thread. Missing rows (auto-created\n    # temp threads) and NULL-owner rows (shared / pre-auth data) stay accessible\n    # via check_access; only a thread already owned by another user is rejected\n    # with 404, matching thread_runs.py's anti-enumeration behaviour. Internal\n    # channel runs act on behalf of the connection owner carried in\n    # X-DeerFlow-Owner-User-Id, so they are scoped to that owner instead of\n    # bypassing the check -- a leaked internal token must not grant cross-user\n    # thread access.\n    user = getattr(request.state, \"user\", None)\n    if user is not None:\n        allowed = await run_ctx.thread_store.check_access(thread_id, str(user.id))\n        if not allowed and owner_user_id and getattr(user, \"system_role\", None) == INTERNAL_SYSTEM_ROLE:\n            # Channel workers may also act for the connection owner named in\n            # the trusted header (e.g. claiming a legacy default-owned channel\n            # thread for its real owner).\n            allowed = await run_ctx.thread_store.check_access(thread_id, owner_user_id)\n        if not allowed:\n            raise HTTPException(status_code=404, detail=f\"Thread {thread_id} not found\")\n\n    owner_context_token = set_current_user(SimpleNamespace(id=owner_user_id)) if owner_user_id else None\n    try:\n        agent_factory = resolve_agent_factory(body.assistant_id)\n        is_internal_caller = getattr(getattr(request, \"state\", None), \"auth_source\", None) == AUTH_SOURCE_INTERNAL\n        command = getattr(body, \"command\", None)\n        if command and command.get(\"resume\") is not None:\n            graph_input = Command(resume=command[\"resume\"])\n        else:\n            graph_input = normalize_input(body.input, trusted_internal=is_internal_caller)\n        config = build_run_config(thread_id, body.config, body.metadata, assistant_id=body.assistant_id)\n        await apply_checkpoint_to_run_config(config, body=body, thread_id=thread_id, request=request)\n\n        # Merge DeerFlow-specific context overrides into both ``configurable`` and ``context``.\n        # The ``context`` field is a custom extension for the langgraph-compat layer\n        # that carries agent configuration (model_name, thinking_enabled, etc.).\n        # Only agent-relevant keys are forwarded; unknown keys (e.g. thread_id) are ignored.\n        merge_run_context_overrides(config, getattr(body, \"context\", None), internal=is_internal_caller)","sourceCodeStart":1106,"sourceCodeEnd":1142,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/services.py#L1106-L1142","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait api.createRun('thread-from-another-user', body); // 404\n\n// after\nconst { thread_id } = await api.createThread();\nawait api.createRun(thread_id, body);","handlingStrategy":"validation","validationCode":"// Only submit runs to threads the current user created\nconst mine = await api.listThreads();\nconst owned = new Set(mine.filter(t => t.owner_user_id === currentUser.id || t.owner_user_id == null).map(t => t.thread_id));\nif (!owned.has(threadId)) throw new Error('thread not accessible for this user');","typeGuard":null,"tryCatchPattern":"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; }","preventionTips":["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."],"tags":["http","authz","thread","multi-tenant","api"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}