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

  1. Inspect the JSON in the error message — it shows exactly which field is missing or malformed.
  2. Check for intercepting proxies/captive portals returning HTML with 200; bypass the proxy (NO_PROXY) and retry.
  3. Align server and client versions so the response matches the DeviceCode schema.
  4. Update dev fixtures/stub servers to return all required fields including a positive numeric expires_in.
  5. 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

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


AI-assisted analysis of JuliusBrussee/caveman@df2ccd85c9 (2026-08-31). Data as JSON: /api/errors/c7eeee2ab502182b. Report an issue: GitHub.