bytedance/deer-flow · error · HTTPException

Thread {thread_id} not found

Error message

Thread {thread_id} not found

What it means

HTTP 404 raised by require_permission's owner check: either require_existing=true and the thread row does not exist, or the thread exists but is owned by a different user_id, or access via the internal-owner header failed check_access. The 404 (not 403) is deliberate — it does not leak the existence of threads the caller does not own. Note check_access returns True for missing rows (legacy untracked threads) and NULL user_id rows (shared/pre-auth data), so this fires only for an existing row owned by someone else, or a genuinely missing thread when require_existing is set.

Source

Thrown at backend/app/gateway/authz.py:524

                )
                if not allowed and getattr(auth.user, "system_role", None) == INTERNAL_SYSTEM_ROLE:
                    # Trusted internal callers (channel workers) also act for
                    # the connection owner carried in X-DeerFlow-Owner-User-Id.
                    # Scope the check to that owner instead of bypassing it; a
                    # leaked internal token must not grant cross-user thread
                    # access. The header is honored only after ``auth`` proved
                    # the caller holds the internal token (mirrors
                    # get_trusted_internal_owner_user_id, which keys off the
                    # middleware-stamped ``request.state.user``).
                    header_owner = (request.headers.get(INTERNAL_OWNER_USER_ID_HEADER_NAME) or "").strip()
                    if header_owner:
                        allowed = await thread_store.check_access(
                            thread_id,
                            header_owner,
                            require_existing=require_existing,
                        )
                if not allowed:
                    raise HTTPException(
                        status_code=404,
                        detail=f"Thread {thread_id} not found",
                    )

            return await func(*args, **kwargs)

        return wrapper

    return decorator

View on GitHub (pinned to 1dd6ba1acb)

Solutions

  1. Use a thread_id that belongs to the authenticated user (re-fetch the thread list)
  2. If shared access is intended, have an owner/admin share or reassign the thread's user_id
  3. Handle the 404 in the client by refreshing or dropping the stale thread from local state
Defensive patterns

Strategy: validation

Validate before calling

# Verify the thread is visible to this user before mutating it
listing = await client.get(f"{base}/api/threads", headers=headers)
owned = {t["thread_id"] for t in listing.json()}
if thread_id not in owned:
    refresh_or_drop_thread(thread_id)  # stale or foreign id — do not call

Try / catch

try:
    await client.get(f"{base}/api/threads/{thread_id}", headers=headers)
except HTTPStatusError as e:
    if e.response.status_code == 404:
        # missing OR owned by someone else — treat identically, refresh local state
        remove_from_thread_list(thread_id)
    else:
        raise

Prevention

When it happens

Trigger: Passing another user's thread_id to a thread-scoped endpoint; passing a thread_id that was deleted while require_existing=true; forging the internal-owner header without holding the internal token (the header is only honored after internal-token auth).

Common situations: Frontend keeping a stale thread list after switching accounts; sharing thread URLs between users; deleted-thread race in the UI; cross-user API scripting.

Related errors


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