{"record":{"id":"7c8f20a5e8f292ee","repo":"remix-run/remix","slug":"expected-oauth-provider-to-return-a-json-object","errorCode":null,"errorMessage":"Expected OAuth provider to return a JSON object.","messagePattern":"Expected OAuth provider to return a JSON object\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/auth/src/lib/provider.ts","lineNumber":302,"sourceCode":"            Authorization: `Basic ${encodeBasicAuth(options.clientId, options.clientSecret)}`,\n          }\n        : undefined),\n      ...options.headers,\n    },\n    body: params,\n  })\n  let json = await readJson(response)\n\n  if (!response.ok || hasOAuthError(json)) {\n    throw new Error(getOAuthErrorMessage(json, options.fallbackError))\n  }\n\n  return normalizeOAuthTokenResponse(json)\n}\n\nfunction normalizeOAuthTokenResponse(json: unknown): OAuthTokens {\n  if (typeof json !== 'object' || json == null || Array.isArray(json)) {\n    throw new Error('Expected OAuth provider to return a JSON object.')\n  }\n\n  let data = json as Record<string, unknown>\n\n  if (typeof data.access_token !== 'string' || data.access_token.length === 0) {\n    throw new Error('OAuth token response did not include an access token.')\n  }\n\n  return {\n    accessToken: data.access_token,\n    refreshToken: typeof data.refresh_token === 'string' ? data.refresh_token : undefined,\n    tokenType: typeof data.token_type === 'string' ? data.token_type : undefined,\n    expiresAt:\n      typeof data.expires_in === 'number'\n        ? new Date(Date.now() + data.expires_in * 1000)\n        : undefined,\n    scope: parseScope(data.scope),\n    idToken: typeof data.id_token === 'string' ? data.id_token : undefined,","sourceCodeStart":284,"sourceCodeEnd":320,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/auth/src/lib/provider.ts#L284-L320","documentation":"Thrown by normalizeOAuthTokenResponse after the OAuth token endpoint responds. The provider's token exchange endpoint returned HTTP 200 but the body parsed to something other than a JSON object (e.g. an array, a string, a number, or null). The library validates the shape before extracting access_token/refresh_token.","triggerScenarios":"Any OAuth provider's token endpoint (Facebook, GitHub, OIDC) returns a 200 response whose JSON body is not a plain object — for example a JSON array, a bare string, or `null`. Reached via exchangeOAuthTokens during handleCallback or refreshTokens.","commonSituations":"Misconfigured token endpoint URL that returns JSON of an unexpected shape (e.g. hitting a list endpoint), a proxy/gateway that rewrites responses, HTML error pages parsed as JSON, or an upstream API version change altering the response envelope.","solutions":["Inspect the raw token endpoint response body for the provider (curl the token URL with the same parameters) to see what it actually returns","Verify the token endpoint URL in provider metadata/config is correct and points at the OAuth2 token endpoint, not another API route","Check for proxies, API gateways, or middleware that rewrite the token response","If the provider is non-standard, wrap or adapt its token response before it reaches exchangeOAuthTokens"],"exampleFix":"// before\nlet tokens = await exchangeOAuthTokens({ tokenEndpoint: 'https://provider.example.com/api/v2/tokens', ... })\n\n// after — use the actual OAuth2 token endpoint\nlet tokens = await exchangeOAuthTokens({ tokenEndpoint: 'https://provider.example.com/oauth/token', ... })","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isJsonObject(value: unknown): value is Record<string, unknown> {\n  return typeof value === 'object' && value !== null && !Array.isArray(value)\n}","tryCatchPattern":"try {\n  let { tokens } = await provider.handleCallback(request)\n} catch (error) {\n  if (error instanceof Error && error.message === 'Expected OAuth provider to return a JSON object.') {\n    // provider returned malformed token payload — inspect provider config/endpoint\n  }\n  throw error\n}","preventionTips":["Verify token endpoint URLs against the provider's current OAuth docs","Log raw provider responses in non-production environments","Avoid custom fetch wrappers that alter response bodies"],"tags":["oauth","token-exchange","json-validation"],"backgroundTag":"oauth-token-endpoint-invalid-response","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}