{"record":{"id":"e050449882e3e018","repo":"odysseus-dev/odysseus","slug":"not-found","errorCode":null,"errorMessage":"Not found","messagePattern":"Not found","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"error","filePath":"routes/task_routes.py","lineNumber":1056,"sourceCode":"            {\"name\": \"document_created\", \"description\": \"Fires when a document is created\"},\n            {\"name\": \"memory_added\", \"description\": \"Fires when a memory is added\"},\n            {\"name\": \"research_completed\", \"description\": \"Fires when a research report completes\"},\n            {\"name\": \"email_received\", \"description\": \"Fires when new inbox mail is observed\"},\n            {\"name\": \"skill_added\", \"description\": \"Fires when a new skill is created\"},\n        ]}\n\n    @router.post(\"/{task_id}/webhook/{token}\")\n    async def webhook_trigger(task_id: str, token: str):\n        \"\"\"Unauthenticated endpoint — the token IS the auth.\"\"\"\n        db = SessionLocal()\n        try:\n            task = db.query(ScheduledTask).filter(\n                ScheduledTask.id == task_id,\n                ScheduledTask.webhook_token == token,\n                ScheduledTask.status == \"active\",\n            ).first()\n            if not task:\n                raise HTTPException(404, \"Not found\")\n            if (\n                is_admin_only_task_action(task.task_type, task.action)\n                and not owner_has_admin_task_privileges(task.owner)\n            ):\n                task.status = \"paused\"\n                task.next_run = None\n                db.commit()\n                raise HTTPException(403, f\"Action '{task.action}' requires admin privileges\")\n        finally:\n            db.close()\n        started = await task_scheduler.run_task_now(task_id)\n        if not started:\n            raise HTTPException(409, \"Task is already running\")\n        return {\"ok\": True, \"message\": \"Task triggered via webhook\"}\n\n    @router.post(\"/{task_id}/webhook-regenerate\")\n    async def regenerate_webhook(request: Request, task_id: str):\n        user = _owner(request)","sourceCodeStart":1038,"sourceCodeEnd":1074,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/task_routes.py#L1038-L1074","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before\ncurl -X POST https://host/api/tasks/abc/webhook/OLD_TOKEN   # 404\n# after\ncurl -X POST https://host/api/tasks/abc/webhook/$(get_current_token)  # fetch fresh token first","handlingStrategy":"validation","validationCode":"// Before triggering, verify task state and token freshness\nconst task = await getTask(taskId);\nif (task.status !== 'active' || !task.webhook_token) throw new Error('task not triggerable');\nawait fetch(`${base}/api/tasks/${taskId}/webhook/${encodeURIComponent(task.webhook_token)}`, {method:'POST'});","typeGuard":null,"tryCatchPattern":"if (resp.status === 404) { await refreshTaskAndToken(); scheduleRetry(); }","preventionTips":["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"],"tags":["http","webhook","tasks","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}