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
- 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).
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
- Feature-detect fetch before rendering device-flow UI
- In Node tests, inject fetchImpl explicitly
- Keep the module out of SSR code paths
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
- Request failed (HTTP ${response.status})
- detail || ('HTTP ' + res.status)
- errData.detail || 'Failed to create session'
- HTTP ${response.status}
- HTTP ${res.status} ${res.statusText}${msg ? `: ${msg}` : ''}
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/d412e26f78307bd9.
Report an issue: GitHub.