odysseus-dev/odysseus · error · HTTPException
Not found
Error message
Not found
What it means
HTTP 404 from POST /api/tasks/{task_id}/webhook/{token}. The lookup is a single three-condition query: id must match, webhook_token must match, and status must be 'active'. The 404 is deliberately generic so callers cannot distinguish a bad token from an inactive task, preventing token enumeration.
Source
Thrown at routes/task_routes.py:1056
{"name": "document_created", "description": "Fires when a document is created"},
{"name": "memory_added", "description": "Fires when a memory is added"},
{"name": "research_completed", "description": "Fires when a research report completes"},
{"name": "email_received", "description": "Fires when new inbox mail is observed"},
{"name": "skill_added", "description": "Fires when a new skill is created"},
]}
@router.post("/{task_id}/webhook/{token}")
async def webhook_trigger(task_id: str, token: str):
"""Unauthenticated endpoint — the token IS the auth."""
db = SessionLocal()
try:
task = db.query(ScheduledTask).filter(
ScheduledTask.id == task_id,
ScheduledTask.webhook_token == token,
ScheduledTask.status == "active",
).first()
if not task:
raise HTTPException(404, "Not found")
if (
is_admin_only_task_action(task.task_type, task.action)
and not owner_has_admin_task_privileges(task.owner)
):
task.status = "paused"
task.next_run = None
db.commit()
raise HTTPException(403, f"Action '{task.action}' requires admin privileges")
finally:
db.close()
started = await task_scheduler.run_task_now(task_id)
if not started:
raise HTTPException(409, "Task is already running")
return {"ok": True, "message": "Task triggered via webhook"}
@router.post("/{task_id}/webhook-regenerate")
async def regenerate_webhook(request: Request, task_id: str):
user = _owner(request)View on GitHub (pinned to f9235ebbf1)
Solutions
- Re-fetch the current webhook_token from the task detail endpoint and confirm status=='active' before triggering
- If the task is paused, resume it (set status back to 'active') and retry
- Regenerate the token via POST /api/tasks/{task_id}/webhook-regenerate and update the external caller with the new URL
- URL-encode the token if it may contain characters that get mangled in transit
Example fix
# before curl -X POST https://host/api/tasks/abc/webhook/OLD_TOKEN # 404 # after curl -X POST https://host/api/tasks/abc/webhook/$(get_current_token) # fetch fresh token first
Defensive patterns
Strategy: validation
Validate before calling
// Before triggering, verify task state and token freshness
const task = await getTask(taskId);
if (task.status !== 'active' || !task.webhook_token) throw new Error('task not triggerable');
await fetch(`${base}/api/tasks/${taskId}/webhook/${encodeURIComponent(task.webhook_token)}`, {method:'POST'}); Try / catch
if (resp.status === 404) { await refreshTaskAndToken(); scheduleRetry(); } Prevention
- Store the full webhook URL, not just the token, and refresh it whenever the task is edited
- Treat 404 as 'invalid or inactive' — re-fetch, never brute-force
- After regenerating a token, update every external caller the same day
When it happens
Trigger: Calling the webhook with a stale token after it was regenerated via /webhook-regenerate; triggering a task whose status is 'paused' or anything other than 'active' (including one auto-paused by the admin-privilege guard); a typo'd or truncated task_id or token in the URL.
Common situations: Token rotated for security and the old cron job / external scheduler still fires the old URL; task paused manually or by the system while the external trigger was not updated; trailing whitespace or URL-encoding damage when copying the token.
Related errors
- Action '{task.action}' requires admin privileges
- Failed to create task
- Failed to update task
- Failed to delete task
- Failed to pause task
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/e050449882e3e018.
Report an issue: GitHub.