JuliusBrussee/caveman · error · Error
device authorization failed: ${JSON.stringify(rawCode)}
Error message
device authorization failed: ${JSON.stringify(rawCode)} What it means
After a successful HTTP response from /auth/device/code, the library validates the parsed JSON against the DeviceCode shape (device_code, user_code, verification_uri as non-empty strings; expires_in a positive finite number). If the payload doesn't match, it throws with the raw payload serialized so you can see exactly what came back.
Source
Thrown at packages/device-auth/src/index.ts:107
signal?: AbortSignal;
sleep?: (ms: number) => Promise<void>;
onCode?: (code: DeviceCode) => void | Promise<void>;
}): Promise<DeviceGrant> {
const fetcher = options.fetch ?? globalThis.fetch;
const wait = options.sleep ?? defaultSleep;
const baseURL = options.baseURL.replace(/\/$/, "");
const codeResponse = await fetcher(`${baseURL}/api/v1/auth/device/code`, {
method: "POST",
headers: { "content-type": "application/json", "x-cave-client": options.client },
body: "{}",
signal: requestSignal(options.signal, 5000),
});
if (!codeResponse.ok) throw new Error(`device authorization failed: HTTP ${codeResponse.status}`);
const rawCode = await codeResponse.json().catch(() => null) as Partial<DeviceCode> | null;
if (rawCode === null || typeof rawCode.device_code !== "string" || rawCode.device_code === "" ||
typeof rawCode.user_code !== "string" || typeof rawCode.verification_uri !== "string" ||
typeof rawCode.expires_in !== "number" || !Number.isFinite(rawCode.expires_in) || rawCode.expires_in <= 0) {
throw new Error(`device authorization failed: ${JSON.stringify(rawCode)}`);
}
const code = rawCode as DeviceCode;
await options.onCode?.(structuredClone(code));
let intervalMs = Math.max(0, Number(code.interval ?? 5)) * 1000;
const deadline = Date.now() + code.expires_in * 1000;
while (Date.now() < deadline) {
let payload: Record<string, unknown>;
let status = 0;
let retryAfterMs = 0;
try {
const response = await fetcher(`${baseURL}/api/v1/auth/device/token`, {
method: "POST",
headers: { "content-type": "application/json", "x-cave-client": options.client },
body: JSON.stringify({ device_code: code.device_code }),
signal: requestSignal(options.signal, 5000),
});
status = response.status;
const retryAfter = response.headers.get("retry-after");View on GitHub (pinned to df2ccd85c9)
Solutions
- Inspect the JSON in the error message — it shows exactly which field is missing or malformed.
- Check for intercepting proxies/captive portals returning HTML with 200; bypass the proxy (NO_PROXY) and retry.
- Align server and client versions so the response matches the DeviceCode schema.
- Update dev fixtures/stub servers to return all required fields including a positive numeric expires_in.
- If the server returns an error envelope with 200, fix the server to use proper HTTP status codes (which then surfaces as the HTTP-status error instead).
Example fix
// before (stub response missing fields)
{ "device_code": "" }
// after
{ "device_code": "dc_123", "user_code": "ABCD-EFGH", "verification_uri": "https://example.com/device", "expires_in": 600 } Defensive patterns
Strategy: type-guard
Validate before calling
const body = await fetch(`${baseURL}/api/v1/auth/device/code`, { method: "POST" }).then(r => r.json()).catch(() => null);
if (!body || typeof body.device_code !== "string" || !body.device_code) throw new Error("server returned malformed device code"); Type guard
function isDeviceCode(v: unknown): v is DeviceCode {
const c = v as Partial<DeviceCode> | null;
return c !== null && typeof c.device_code === "string" && c.device_code !== ""
&& typeof c.user_code === "string" && typeof c.verification_uri === "string"
&& typeof c.expires_in === "number" && Number.isFinite(c.expires_in) && c.expires_in > 0;
} Try / catch
try {
await runCavemanDeviceFlow(options);
} catch (e) {
if (e instanceof Error && e.message.startsWith("device authorization failed: {")) {
console.error(`Malformed device code payload: ${e.message} — check for proxy HTML responses or API drift`);
} else throw e;
} Prevention
- Bypass captive portals/intercepting proxies when hitting the auth API.
- Keep client and server DeviceCode schemas in sync; add contract tests.
- Give dev stubs full valid responses including numeric expires_in > 0.
- Fix servers to return real HTTP error codes rather than 200-with-error-body.
When it happens
Trigger: The code endpoint returns 2xx but the body is malformed: an error object, HTML (parsed to null), missing or empty device_code/user_code/verification_uri, or expires_in absent/non-numeric/<=0.
Common situations: A proxy or captive portal returning an HTML login page with status 200; server/client contract drift after an API update; a dev stub returning incomplete fixtures; gateway returning 200 with an error envelope.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- cave_live_eval_sandbox_profile_invalid
- device login failed: server did not provide a delivery ackno
- caveman build: invalid .caveman/provider.json
- caveman agent: value is not canonically serializable
- caveman agent: invalid .caveman/provider.json
AI-assisted analysis of JuliusBrussee/caveman@df2ccd85c9 (2026-08-31).
Data as JSON: /api/errors/c7eeee2ab502182b.
Report an issue: GitHub.