{"record":{"id":"89cb48ee3f2accc6","repo":"can1357/oh-my-pi","slug":"label-was-not-a-json-object","errorCode":null,"errorMessage":"${label} was not a JSON object","messagePattern":"(.+?) was not a JSON object","errorType":"validation","errorClass":"AIError.OAuthError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/registry/oauth/xai-oauth.ts","lineNumber":326,"sourceCode":"\t\t\tkind: \"validation\",\n\t\t\tprovider: \"xai\",\n\t\t});\n\t}\n\n\tvalidateXAIEndpoint(verificationUri, \"verification_uri\");\n\tvalidateXAIEndpoint(verificationUriComplete, \"verification_uri_complete\");\n\treturn {\n\t\tdeviceCode,\n\t\tuserCode,\n\t\tverificationUriComplete,\n\t\texpiresInSeconds,\n\t\tintervalSeconds,\n\t};\n}\n\nfunction parseXAITokenResponse(payload: unknown, label: string, refreshTokenFallback?: string): OAuthCredentials {\n\tif (!isRecord(payload)) {\n\t\tthrow new AIError.OAuthError(`${label} was not a JSON object`, {\n\t\t\tkind: \"validation\",\n\t\t\tprovider: \"xai\",\n\t\t});\n\t}\n\tconst accessToken = typeof payload.access_token === \"string\" ? payload.access_token : \"\";\n\tconst responseRefreshToken = typeof payload.refresh_token === \"string\" ? payload.refresh_token : \"\";\n\tconst refreshToken = responseRefreshToken || refreshTokenFallback || \"\";\n\tconst expiresInSeconds = payload.expires_in;\n\tif (!accessToken) {\n\t\tthrow new AIError.OAuthError(`${label} missing access_token`, {\n\t\t\tkind: \"validation\",\n\t\t\tprovider: \"xai\",\n\t\t});\n\t}\n\tif (!refreshToken) {\n\t\tthrow new AIError.OAuthError(`${label} missing refresh_token`, {\n\t\t\tkind: \"validation\",\n\t\t\tprovider: \"xai\",","sourceCodeStart":308,"sourceCodeEnd":344,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/registry/oauth/xai-oauth.ts#L308-L344","documentation":"Thrown by parseXAITokenResponse when a token payload (device-flow poll success or refresh via credentials) parses as JSON but is not a plain object. The label parameter identifies which exchange failed (e.g. 'xAI device-code token response'). The library requires an object carrying access_token/refresh_token/expires_in before it can build OAuthCredentials.","triggerScenarios":"pollXAIDeviceToken receives an HTTP 200 whose JSON body is not an object (array/string/number/null), or the credentials refresh path gets a non-object payload back from the token endpoint; parseXAITokenResponse throws with the exchange's label in the message.","commonSituations":"Token endpoint returning a JWE string or other non-object token format; proxy mangling the body; custom fetchImpl double-wrapping responses; xAI changing token response encoding.","solutions":["Retry the token exchange or re-run device login — transient proxy responses are the usual culprit.","Check middleboxes (corporate proxy, VPN) that may rewrite the token endpoint body.","If using a custom fetchImpl, ensure it returns the parsed JSON value as-is without transformation.","Compare the raw token-endpoint response with curl; if xAI changed the format, update the ai package."],"exampleFix":"// before: assuming the token body is an object\nconst tokens = await res.json();\nsaveCredentials(tokens.access_token);\n// after: narrow before use\nconst tokens: unknown = await res.json();\nif (tokens === null || typeof tokens !== \"object\" || Array.isArray(tokens)) {\n  throw new Error(\"token endpoint did not return a JSON object\");\n}\nsaveCredentials((tokens as Record<string, unknown>).access_token);","handlingStrategy":"type-guard","validationCode":"// preflight on the token endpoint: confirm object-shaped JSON before an exchange\nconst probe = await fetch(tokenEndpoint, { method: \"POST\", headers: { Accept: \"application/json\" } });\nconst body: unknown = await probe.json();\nif (body === null || typeof body !== \"object\" || Array.isArray(body)) {\n  throw new Error(\"xAI token endpoint returns non-object JSON in this environment; check proxy\");\n}","typeGuard":"function isJsonObject(v: unknown): v is Record<string, unknown> {\n  return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}","tryCatchPattern":"try {\n  await xaiProvider.credentials();\n} catch (err) {\n  if (err instanceof AIError.OAuthError && err.kind === \"validation\" && err.message.includes(\"was not a JSON object\")) {\n    logger.warn(\"xAI token exchange returned a non-object payload\", {});\n  } else {\n    throw err;\n  }\n}","preventionTips":["Do not wrap/transform token responses in custom fetchImpl implementations.","Watch for token-endpoint formats like JWE strings that parse as non-object JSON.","Test the login flow once per network environment (home, office, VPN) — proxies are the usual culprit.","Keep @oh-my-pi/pi-ai updated against xAI token-endpoint changes."],"tags":["oauth","xai","token-endpoint","response-validation","json"],"backgroundTag":"oauth-response-shape-mismatch","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}