JuliusBrussee/caveman · error
device login polling failed
Error message
device login polling failed: ${error instanceof Error ? error.message : String(error)} What it means
While polling the RFC 8628 token endpoint, transient failures (dropped connections, malformed responses) are retried until a bounded device deadline. If an exception occurs and the deadline has already passed, the CLI gives up and throws this error wrapping the underlying message. Earlier errors while time remains are silently retried.
Solutions
- Re-run login and complete browser approval promptly (the code expires)
- Check network connectivity/VPN stability during the polling window
- Increase the server-side device-code lifetime/interval if you control the instance
- Retry login once the instance is reachable again
Example fix
// before
await sleep(intervalMs); // polling continues past deadline on network failure
// after
if (Date.now() >= deadline) throw new Error(`device login polling failed: ${err.message}`);
await sleep(Math.max(intervalMs, retryAfterMs, 200)); Defensive patterns
Strategy: retry
Validate before calling
const reachable = await fetch(instance, { signal: AbortSignal.timeout(3000) }).then(r => r.ok).catch(() => false);
if (!reachable) console.warn("Instance unreachable; polling may fail before approval"); Try / catch
try { await login({ instance }) } catch (e) { if (e.message.startsWith("device login polling failed:")) await retryLoginWithBackoff(); } Prevention
- Complete browser approval promptly after starting login
- Use a stable network/VPN during login
- Ensure the device-code lifetime exceeds the expected approval time
When it happens
Trigger: An exception is thrown during a token poll AND Date.now() >= deadline — i.e. the last poll before the device-code grant expired failed due to a network error, JSON parse failure, or similar transient fault.
Common situations: User never opened the verification URL, so polling ran the full window; flaky network or VPN dropping late in the polling window; instance becoming unreachable mid-flow; very short device-code lifetime on the server.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
Related errors
- device authorization failed: HTTP
- device credential delivery acknowledgement failed
- AbortError
- awscreds: request failed
- cave_agent_tool_timeout | cave_network_error
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/55361c0c3f5d9733.
Report an issue: GitHub.
Appendix: source
Thrown at packages/cli/src/index.ts:9718
redirect: "manual",
headers: { "content-type": "application/json" },
body: JSON.stringify({ device_code: code.device_code }),
signal: AbortSignal.timeout(5000),
});
tokenStatus = tokResp.status;
const retryAfter = tokResp.headers.get("retry-after");
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds) && seconds >= 0) retryAfterMs = seconds * 1000;
}
tok = tokenStatus >= 300 && tokenStatus < 400 ? {} : await tokResp.json() as Record<string, unknown>;
} catch (error) {
// RFC 8628 polling is retryable: a dropped connection or malformed
// transient response must not consume the approved code or abort login
// before the bounded device deadline. The next poll can reclaim the
// server-side lease and replay the same durable bundle.
if (Date.now() >= deadline) {
throw new Error(`device login polling failed: ${error instanceof Error ? error.message : String(error)}`);
}
await sleep(Math.max(intervalMs, retryAfterMs, 200));
continue;
}
if (tokenStatus >= 300 && tokenStatus < 400) throw new Error("device login refused a redirected token endpoint");
if (tokenStatus === 429) {
// rateLimitAuth returns a nested cave error envelope rather than the RFC
// `error` string. Status is the authoritative retry signal here.
await sleep(Math.max(intervalMs, retryAfterMs, 200));
continue;
}
const accessToken = typeof tok.access_token === "string" ? tok.access_token : "";
if (accessToken) {
if (instance && (tokenStatus < 200 || tokenStatus >= 300 || tok.credential_kind !== "none" ||
["gateway_api_key", "gateway_key_id", "gateway_url"].some((key) => tok[key] != null) ||
typeof tok.refresh_token !== "string" || !tok.refresh_token || typeof tok.project_id !== "string" || !tok.project_id ||
typeof tok.delivery_ack_token !== "string" || !tok.delivery_ack_token || typeof tok.scope !== "string" || !tok.scope ||
tok.scope.split(/\s+/).some((scope) => scope === "proxy:write" || scope === "sdk:write"))) {View on GitHub (pinned to 3ee70a1026)