{"record":{"id":"d412e26f78307bd9","repo":"odysseus-dev/odysseus","slug":"fetch-api-is-unavailable","errorCode":null,"errorMessage":"Fetch API is unavailable","messagePattern":"Fetch API is unavailable","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"static/js/providerDeviceFlow.js","lineNumber":78,"sourceCode":"    throw new Error(_messageFromPayload(payload, fallback || `Request failed (HTTP ${response.status})`));\n  }\n  return payload;\n}\n\nfunction _defaultSleep(ms) {\n  return new Promise(resolve => setTimeout(resolve, ms));\n}\n\nasync function _callCallback(fn, payload) {\n  if (typeof fn === 'function') await fn(payload);\n}\n\nexport async function runProviderDeviceFlow(provider, options = {}) {\n  const cfg = PROVIDER_DEVICE_FLOWS[provider];\n  if (!cfg) throw new Error(`Unknown device-flow provider: ${provider}`);\n\n  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);","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/static/js/providerDeviceFlow.js#L60-L96","documentation":"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.","triggerScenarios":"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.","commonSituations":"Unit tests in an old Node version; SSR environments executing the module server-side; security-hardened webviews where fetch is not exposed.","solutions":["Pass options.fetchImpl explicitly (e.g. wrap require('undici').fetch or global fetch polyfill).","Upgrade to Node >= 18 or a fetch-supporting browser.","Feature-detect before showing device-flow UI (see validationCode)."],"exampleFix":"// before\nawait runProviderDeviceFlow('google', {});\n\n// after\nawait runProviderDeviceFlow('google', { fetchImpl: globalThis.fetch?.bind(globalThis) ?? require('undici').fetch });","handlingStrategy":"validation","validationCode":"if (typeof globalThis.fetch !== 'function' && !options.fetchImpl) { showError('This browser cannot start device sign-in'); return; }","typeGuard":"const hasFetch = (o) => typeof (o?.fetchImpl || globalThis.fetch) === 'function';","tryCatchPattern":"catch (e) { if (/Fetch API is unavailable/.test(e.message)) showError('Please update your browser to use device sign-in'); else throw e; }","preventionTips":["Feature-detect fetch before rendering device-flow UI","In Node tests, inject fetchImpl explicitly","Keep the module out of SSR code paths"],"tags":["environment","fetch","device-flow","compatibility"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}