odysseus-dev/odysseus · error · Error

Fetch API is unavailable

Error message

Fetch API is unavailable

What it means

Thrown by runProviderDeviceFlow when neither options.fetchImpl nor a bound globalThis.fetch exists. This is an environment capability check, not a network failure — the code cannot issue any HTTP request. The optional-chaining bind means ancient browsers or non-browser runtimes without fetch land here.

Source

Thrown at static/js/providerDeviceFlow.js:78

    throw new Error(_messageFromPayload(payload, fallback || `Request failed (HTTP ${response.status})`));
  }
  return payload;
}

function _defaultSleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function _callCallback(fn, payload) {
  if (typeof fn === 'function') await fn(payload);
}

export async function runProviderDeviceFlow(provider, options = {}) {
  const cfg = PROVIDER_DEVICE_FLOWS[provider];
  if (!cfg) throw new Error(`Unknown device-flow provider: ${provider}`);

  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);

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Pass options.fetchImpl explicitly (e.g. wrap require('undici').fetch or global fetch polyfill).
  2. Upgrade to Node >= 18 or a fetch-supporting browser.
  3. Feature-detect before showing device-flow UI (see validationCode).

Example fix

// before
await runProviderDeviceFlow('google', {});

// after
await runProviderDeviceFlow('google', { fetchImpl: globalThis.fetch?.bind(globalThis) ?? require('undici').fetch });
Defensive patterns

Strategy: validation

Validate before calling

if (typeof globalThis.fetch !== 'function' && !options.fetchImpl) { showError('This browser cannot start device sign-in'); return; }

Type guard

const hasFetch = (o) => typeof (o?.fetchImpl || globalThis.fetch) === 'function';

Try / catch

catch (e) { if (/Fetch API is unavailable/.test(e.message)) showError('Please update your browser to use device sign-in'); else throw e; }

Prevention

When it happens

Trigger: Running the module in Node < 18 (no global fetch) without passing options.fetchImpl; a sandboxed iframe/worker where fetch is stripped; a very old browser (pre-2017) loading the bundle.

Common situations: Unit tests in an old Node version; SSR environments executing the module server-side; security-hardened webviews where fetch is not exposed.

Related errors


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