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

  1. Inspect the actual start response in the network tab to see which field carries the poll identifier.
  2. Align client and server versions so the poll_id contract matches.
  3. If behind a transforming proxy, exclude the auth routes from rewriting.
  4. 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

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


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/29ecdca6d1c1655f. Report an issue: GitHub.