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
- Use a thread_id that belongs to the authenticated user (re-fetch the thread list)
- If shared access is intended, have an owner/admin share or reassign the thread's user_id
- 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
- Always source thread ids from the authenticated user's own thread list response
- On account switch or logout, clear cached thread state
- Treat 404 as 'not yours or gone' — do not retry with the same credentials
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
- Authentication required
- Permission denied: {resource}:{action}
- Thread not found
- Thread metadata store not available
- NOT_AUTHENTICATED
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/6f0149e9651fabd4.
Report an issue: GitHub.