odysseus-dev/odysseus · error · Error
${cfg.label} sign-in did not return a poll id
Error message
${cfg.label} sign-in did not return a poll id What it means
Thrown after a successful start call when the response JSON lacks poll_id. The device-flow cannot proceed because the subsequent poll loop has nothing to poll; the message names the provider label for user-facing display. This is a server contract violation: HTTP 2xx but malformed payload.
Source
Thrown at static/js/providerDeviceFlow.js:95
const fetchImpl = options.fetchImpl || globalThis.fetch?.bind(globalThis);
if (!fetchImpl) throw new Error('Fetch API is unavailable');
const openWindow = options.openWindow || ((url) => {
if (globalThis.window && typeof globalThis.window.open === 'function') {
globalThis.window.open(url, '_blank', 'noopener');
}
});
const sleep = options.sleep || _defaultSleep;
const now = options.now || (() => Date.now());
const formData = options.formData || _formData();
const start = await _fetchJson(fetchImpl, cfg.startUrl, {
method: 'POST',
body: formData,
credentials: 'same-origin',
}, `Failed to start ${cfg.label} sign-in`);
if (!start.poll_id) throw new Error(`${cfg.label} sign-in did not return a poll id`);
const authUrl = cfg.authUrl(start);
await _callCallback(options.onStart, { provider, config: cfg, start, authUrl });
if (authUrl) openWindow(authUrl);
const deadline = now() + Number(start.expires_in || 900) * 1000;
let stepMs = Math.max(Number(start.interval || 5), 2) * 1000;
while (true) {
if (now() > deadline) return { status: 'expired' };
await _callCallback(options.onWaiting, { provider, config: cfg, start, authUrl });
await sleep(stepMs);
if (now() > deadline) return { status: 'expired' };
const fd = _formData();
fd.append('poll_id', start.poll_id);
const poll = await _fetchJson(fetchImpl, cfg.pollUrl, {
method: 'POST',
body: fd,View on GitHub (pinned to f9235ebbf1)
Solutions
- Inspect the actual start response in the network tab to see which field carries the poll identifier.
- Align client and server versions so the poll_id contract matches.
- If behind a transforming proxy, exclude the auth routes from rewriting.
- Fix test fixtures to include poll_id.
Defensive patterns
Strategy: type-guard
Type guard
const isDeviceStartPayload = (p) => !!p && (typeof p.poll_id === 'string' || typeof p.poll_id === 'number');
Try / catch
try { const start = await _fetchJson(...); if (!isDeviceStartPayload(start)) throw new Error(`${cfg.label} sign-in did not return a poll id`); } catch (e) { formatDeviceFlowError(e); } Prevention
- Return explicit error statuses instead of 200-with-missing-fields from the start endpoint
- Pin client and server to the same contract version
- Integration-test the start response shape
When it happens
Trigger: The start endpoint returning {device_code:...} with a different field name than poll_id; a proxy returning a 200 status page instead of JSON; backend version where the contract changed (e.g. poll_id renamed); start endpoint returning an empty object on partial failure.
Common situations: Frontend/backend version skew after an API refactor; middleware rewriting responses; a mock server in tests returning incomplete fixtures.
Related errors
- Request failed (HTTP ${response.status})
- Unknown device-flow provider: ${provider}
- Server returned no session id
- Session request returned an invalid response
- 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/29ecdca6d1c1655f.
Report an issue: GitHub.