{"record":{"id":"b68812af6580ed59","repo":"odysseus-dev/odysseus","slug":"unknown-or-expired-login-session","errorCode":null,"errorMessage":"Unknown or expired login session","messagePattern":"Unknown or expired login session","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"warning","filePath":"routes/device_flow.py","lineNumber":164,"sourceCode":"\n    @router.post(\"/device/start\")\n    async def device_start(request: Request):\n        require_admin(request)\n        form = await request.form()\n        start = await _maybe_await(start_flow(request, form))\n        interval = int(start.interval or 5)\n        expires_in = int(start.expires_in or 900)\n        poll_id = store.add(start.pending, interval=interval, expires_in=expires_in)\n        response = dict(start.response)\n        response.update({\"poll_id\": poll_id, \"interval\": interval, \"expires_in\": expires_in})\n        return response\n\n    @router.post(\"/device/poll\")\n    async def device_poll(request: Request, poll_id: str = Form(...)):\n        require_admin(request)\n        payload = store.get_payload(poll_id)\n        if payload is None:\n            raise HTTPException(404, \"Unknown or expired login session\")\n        if store.is_throttled(poll_id):\n            return {\"status\": \"pending\"}\n\n        try:\n            outcome = await _maybe_await(poll_flow(request, payload))\n        except Exception:\n            store.pop(poll_id)\n            raise\n\n        if outcome.status == \"authorized\":\n            store.pop(poll_id)\n            return {\"status\": \"authorized\", \"endpoint\": dict(outcome.endpoint or {})}\n        if outcome.status == \"failed\":\n            store.pop(poll_id)\n            return {\"status\": \"failed\", \"error\": outcome.error or \"denied\"}\n        if outcome.status == \"slow_down\":\n            store.slow_down(poll_id, outcome.interval)\n            return _pending_response(outcome.detail)","sourceCodeStart":146,"sourceCodeEnd":182,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/device_flow.py#L146-L182","documentation":"Raised (HTTP 404) by POST /device/poll (generic device-flow poll endpoint, admin-gated) when store.get_payload(poll_id) returns None — the in-memory session store has no record for that poll_id, or the entry expired (store entries carry expires_in, default 900s from the start response). The device-flow session is created by the start endpoint and identified by the opaque poll_id returned there.","triggerScenarios":"Polling with a poll_id that was never issued (typo/truncated ID); polling after the session expired (device codes themselves expire in ~15 minutes and the store mirrors that); polling after the session was already popped — e.g. a previous poll returned 'authorized'/'failed' and the client sends one more poll; server restart wiped the in-memory store.","commonSituations":"Frontend keeps polling after success because a race duplicated the poll request; the user leaves the login tab open past expiry and the resume poll 404s; the app process restarted between start and poll, dropping all stored sessions.","solutions":["Restart the login flow: call the device start endpoint again and use the fresh poll_id it returns.","Make the client stop polling once it receives a terminal status ('authorized' or 'failed') — the store entry is popped at that moment.","Poll within the expires_in window returned by start (default 900 seconds).","If the server restarted mid-flow, expect all outstanding poll_ids to 404; every user mid-login must restart the flow."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Client-side session liveness: stop polling on terminal states and before expiry\nclass DevicePoller {\n  constructor(startResp) { this.id = startResp.poll_id; this.deadline = Date.now() + startResp.expires_in * 1000; }\n  get expired() { return Date.now() > this.deadline; }\n  shouldPoll(lastStatus) { return !this.expired && lastStatus !== 'authorized' && lastStatus !== 'failed'; }\n}","typeGuard":null,"tryCatchPattern":"try { r = await post('/device/poll', { poll_id }); } catch (e) { if (e.status === 404 && /Unknown or expired/.test(e.message)) { return restartDeviceFlow(); } throw e; }","preventionTips":["Stop polling immediately after 'authorized'/'failed' — the server pops the session then.","Honor expires_in from the start response; restart the flow instead of polling past it.","After any server restart, discard cached poll_ids."],"tags":["device-flow","oauth","session-store","expiry","polling"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}