odysseus-dev/odysseus · warning · HTTPException
Unknown or expired login session
Error message
Unknown or expired login session
What it means
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.
Source
Thrown at routes/device_flow.py:164
@router.post("/device/start")
async def device_start(request: Request):
require_admin(request)
form = await request.form()
start = await _maybe_await(start_flow(request, form))
interval = int(start.interval or 5)
expires_in = int(start.expires_in or 900)
poll_id = store.add(start.pending, interval=interval, expires_in=expires_in)
response = dict(start.response)
response.update({"poll_id": poll_id, "interval": interval, "expires_in": expires_in})
return response
@router.post("/device/poll")
async def device_poll(request: Request, poll_id: str = Form(...)):
require_admin(request)
payload = store.get_payload(poll_id)
if payload is None:
raise HTTPException(404, "Unknown or expired login session")
if store.is_throttled(poll_id):
return {"status": "pending"}
try:
outcome = await _maybe_await(poll_flow(request, payload))
except Exception:
store.pop(poll_id)
raise
if outcome.status == "authorized":
store.pop(poll_id)
return {"status": "authorized", "endpoint": dict(outcome.endpoint or {})}
if outcome.status == "failed":
store.pop(poll_id)
return {"status": "failed", "error": outcome.error or "denied"}
if outcome.status == "slow_down":
store.slow_down(poll_id, outcome.interval)
return _pending_response(outcome.detail)View on GitHub (pinned to f9235ebbf1)
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.
Defensive patterns
Strategy: validation
Validate before calling
// Client-side session liveness: stop polling on terminal states and before expiry
class DevicePoller {
constructor(startResp) { this.id = startResp.poll_id; this.deadline = Date.now() + startResp.expires_in * 1000; }
get expired() { return Date.now() > this.deadline; }
shouldPoll(lastStatus) { return !this.expired && lastStatus !== 'authorized' && lastStatus !== 'failed'; }
} Try / catch
try { r = await post('/device/poll', { poll_id }); } catch (e) { if (e.status === 404 && /Unknown or expired/.test(e.message)) { return restartDeviceFlow(); } throw e; } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Request failed (HTTP ${response.status})
- Unknown device-flow provider: ${provider}
- ${cfg.label} sign-in did not return a poll id
- HTTP ${r.status}
- ChatGPT token response was missing access_token or refresh_t
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/b68812af6580ed59.
Report an issue: GitHub.