{"record":{"id":"29ecdca6d1c1655f","repo":"odysseus-dev/odysseus","slug":"cfg-label-sign-in-did-not-return-a-poll-id","errorCode":null,"errorMessage":"${cfg.label} sign-in did not return a poll id","messagePattern":"(.+?) sign-in did not return a poll id","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/js/providerDeviceFlow.js","lineNumber":95,"sourceCode":"  const fetchImpl = options.fetchImpl || globalThis.fetch?.bind(globalThis);\n  if (!fetchImpl) throw new Error('Fetch API is unavailable');\n\n  const openWindow = options.openWindow || ((url) => {\n    if (globalThis.window && typeof globalThis.window.open === 'function') {\n      globalThis.window.open(url, '_blank', 'noopener');\n    }\n  });\n  const sleep = options.sleep || _defaultSleep;\n  const now = options.now || (() => Date.now());\n  const formData = options.formData || _formData();\n\n  const start = await _fetchJson(fetchImpl, cfg.startUrl, {\n    method: 'POST',\n    body: formData,\n    credentials: 'same-origin',\n  }, `Failed to start ${cfg.label} sign-in`);\n\n  if (!start.poll_id) throw new Error(`${cfg.label} sign-in did not return a poll id`);\n  const authUrl = cfg.authUrl(start);\n  await _callCallback(options.onStart, { provider, config: cfg, start, authUrl });\n  if (authUrl) openWindow(authUrl);\n\n  const deadline = now() + Number(start.expires_in || 900) * 1000;\n  let stepMs = Math.max(Number(start.interval || 5), 2) * 1000;\n\n  while (true) {\n    if (now() > deadline) return { status: 'expired' };\n    await _callCallback(options.onWaiting, { provider, config: cfg, start, authUrl });\n    await sleep(stepMs);\n    if (now() > deadline) return { status: 'expired' };\n\n    const fd = _formData();\n    fd.append('poll_id', start.poll_id);\n    const poll = await _fetchJson(fetchImpl, cfg.pollUrl, {\n      method: 'POST',\n      body: fd,","sourceCodeStart":77,"sourceCodeEnd":113,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/providerDeviceFlow.js#L77-L113","documentation":"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.","triggerScenarios":"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.","commonSituations":"Frontend/backend version skew after an API refactor; middleware rewriting responses; a mock server in tests returning incomplete fixtures.","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."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":null,"typeGuard":"const isDeviceStartPayload = (p) => !!p && (typeof p.poll_id === 'string' || typeof p.poll_id === 'number');","tryCatchPattern":"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); }","preventionTips":["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"],"tags":["oauth","device-flow","contract-violation","api-shape"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}