{"record":{"id":"c7eeee2ab502182b","repo":"JuliusBrussee/caveman","slug":"device-authorization-failed-json-stringify-rawc","errorCode":null,"errorMessage":"device authorization failed: ${JSON.stringify(rawCode)}","messagePattern":"device authorization failed: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/device-auth/src/index.ts","lineNumber":107,"sourceCode":"  signal?: AbortSignal;\n  sleep?: (ms: number) => Promise<void>;\n  onCode?: (code: DeviceCode) => void | Promise<void>;\n}): Promise<DeviceGrant> {\n  const fetcher = options.fetch ?? globalThis.fetch;\n  const wait = options.sleep ?? defaultSleep;\n  const baseURL = options.baseURL.replace(/\\/$/, \"\");\n  const codeResponse = await fetcher(`${baseURL}/api/v1/auth/device/code`, {\n    method: \"POST\",\n    headers: { \"content-type\": \"application/json\", \"x-cave-client\": options.client },\n    body: \"{}\",\n    signal: requestSignal(options.signal, 5000),\n  });\n  if (!codeResponse.ok) throw new Error(`device authorization failed: HTTP ${codeResponse.status}`);\n  const rawCode = await codeResponse.json().catch(() => null) as Partial<DeviceCode> | null;\n  if (rawCode === null || typeof rawCode.device_code !== \"string\" || rawCode.device_code === \"\" ||\n    typeof rawCode.user_code !== \"string\" || typeof rawCode.verification_uri !== \"string\" ||\n    typeof rawCode.expires_in !== \"number\" || !Number.isFinite(rawCode.expires_in) || rawCode.expires_in <= 0) {\n    throw new Error(`device authorization failed: ${JSON.stringify(rawCode)}`);\n  }\n  const code = rawCode as DeviceCode;\n  await options.onCode?.(structuredClone(code));\n  let intervalMs = Math.max(0, Number(code.interval ?? 5)) * 1000;\n  const deadline = Date.now() + code.expires_in * 1000;\n  while (Date.now() < deadline) {\n    let payload: Record<string, unknown>;\n    let status = 0;\n    let retryAfterMs = 0;\n    try {\n      const response = await fetcher(`${baseURL}/api/v1/auth/device/token`, {\n        method: \"POST\",\n        headers: { \"content-type\": \"application/json\", \"x-cave-client\": options.client },\n        body: JSON.stringify({ device_code: code.device_code }),\n        signal: requestSignal(options.signal, 5000),\n      });\n      status = response.status;\n      const retryAfter = response.headers.get(\"retry-after\");","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/df2ccd85c94ec3c8289cb62ac020d241ccfb0c60/packages/device-auth/src/index.ts#L89-L125","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before (stub response missing fields)\n{ \"device_code\": \"\" }\n// after\n{ \"device_code\": \"dc_123\", \"user_code\": \"ABCD-EFGH\", \"verification_uri\": \"https://example.com/device\", \"expires_in\": 600 }","handlingStrategy":"type-guard","validationCode":"const body = await fetch(`${baseURL}/api/v1/auth/device/code`, { method: \"POST\" }).then(r => r.json()).catch(() => null);\nif (!body || typeof body.device_code !== \"string\" || !body.device_code) throw new Error(\"server returned malformed device code\");","typeGuard":"function isDeviceCode(v: unknown): v is DeviceCode {\n  const c = v as Partial<DeviceCode> | null;\n  return c !== null && typeof c.device_code === \"string\" && c.device_code !== \"\"\n    && typeof c.user_code === \"string\" && typeof c.verification_uri === \"string\"\n    && typeof c.expires_in === \"number\" && Number.isFinite(c.expires_in) && c.expires_in > 0;\n}","tryCatchPattern":"try {\n  await runCavemanDeviceFlow(options);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith(\"device authorization failed: {\")) {\n    console.error(`Malformed device code payload: ${e.message} — check for proxy HTML responses or API drift`);\n  } else throw e;\n}","preventionTips":["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."],"tags":["device-auth","schema-validation","api-contract","json"],"backgroundTag":"schema-validation-failed","analyzedSha":"df2ccd85c94ec3c8289cb62ac020d241ccfb0c60","analyzedAt":"2026-08-31T22:10:17.934Z","contentChangedAt":"2026-08-31T22:10:17.934Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}