JuliusBrussee/caveman · error · Error
device login failed: ${errorCode}
Error message
device login failed: ${errorCode} What it means
While polling the token endpoint, the OAuth device grant can return an error field. authorization_pending and slow_down are expected and handled; any other error code (access_denied, expired_token, invalid_client, etc.) is fatal and thrown with the raw code embedded.
Source
Thrown at packages/device-auth/src/index.ts:168
async acknowledge() {
if (acknowledged) return;
await acknowledge({
baseURL,
client: options.client,
code,
credentials,
fetcher,
...(options.signal === undefined ? {} : { signal: options.signal }),
sleep: wait,
});
acknowledged = true;
},
};
}
const errorCode = typeof payload.error === "string" ? payload.error : "";
if (errorCode === "slow_down") intervalMs = nextDevicePollIntervalMs(intervalMs, errorCode);
else if (errorCode !== "" && errorCode !== "authorization_pending") {
throw new Error(`device login failed: ${errorCode}`);
}
await wait(Math.max(intervalMs, 200));
}
throw new Error("device login timed out before approval");
}
View on GitHub (pinned to df2ccd85c9)
Solutions
- Read the embedded code: access_denied means the user declined — restart the flow and approve the request.
- If expired_token, restart the device flow to get a fresh device code.
- Check the client identifier sent to /auth/device/code matches a registered client (invalid_client).
- Verify the account/tenant permits device authorization (org policy may deny it, surfacing as access_denied).
- If the code is unexpected, compare it against the OAuth device-grant error registry to find the server-side cause.
Example fix
// before (user denied, then blindly retrying the same flow)
try { await runCavemanDeviceFlow(opts); } catch { await runCavemanDeviceFlow(opts); }
// after
try { await runCavemanDeviceFlow(opts); } catch (e) {
if (String(e).includes("access_denied")) prompt("You must approve the login to continue");
await runCavemanDeviceFlow(opts); // restart with fresh code after explaining
} Defensive patterns
Strategy: try-catch
Try / catch
try {
await runCavemanDeviceFlow(options);
} catch (e) {
if (e instanceof Error && e.message.startsWith("device login failed: ")) {
const code = e.message.slice("device login failed: ".length);
if (code === "access_denied") console.error("Login was denied — approve the request and retry");
else if (code === "expired_token") console.error("Code expired — restarting flow");
else throw e;
} else throw e;
} Prevention
- Instruct users to click Approve, not Cancel, on the verification page.
- Complete the flow before the device code expires (expires_in).
- Keep the client identifier registered/valid with the auth server.
- Handle access_denied and expired_token explicitly in UX instead of surfacing raw errors.
When it happens
Trigger: The token endpoint responds with a JSON payload whose error is a string other than "authorization_pending" or "slow_down" — most commonly access_denied (user rejected) or expired_token.
Common situations: User clicks 'Cancel'/'Deny' on the verification page (access_denied); the device code expired server-side between polls (expired_token); client credentials rejected (invalid_client) after a client-id change; account/tenant restrictions blocking the grant.
Related errors
- device login polling failed: ${error instanceof Error ? erro
- device login failed: server did not provide a delivery ackno
- device credential delivery acknowledgement failed (${lastErr
- device authorization failed: HTTP ${codeResponse.status}
- device authorization failed: ${JSON.stringify(rawCode)}
AI-assisted analysis of JuliusBrussee/caveman@df2ccd85c9 (2026-08-31).
Data as JSON: /api/errors/48b1e8d4252fefef.
Report an issue: GitHub.