remix-run/remix · error · Error
Expected OAuth provider to return a JSON object.
Error message
Expected OAuth provider to return a JSON object.
What it means
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.
Source
Thrown at packages/auth/src/lib/provider.ts:302
Authorization: `Basic ${encodeBasicAuth(options.clientId, options.clientSecret)}`,
}
: undefined),
...options.headers,
},
body: params,
})
let json = await readJson(response)
if (!response.ok || hasOAuthError(json)) {
throw new Error(getOAuthErrorMessage(json, options.fallbackError))
}
return normalizeOAuthTokenResponse(json)
}
function normalizeOAuthTokenResponse(json: unknown): OAuthTokens {
if (typeof json !== 'object' || json == null || Array.isArray(json)) {
throw new Error('Expected OAuth provider to return a JSON object.')
}
let data = json as Record<string, unknown>
if (typeof data.access_token !== 'string' || data.access_token.length === 0) {
throw new Error('OAuth token response did not include an access token.')
}
return {
accessToken: data.access_token,
refreshToken: typeof data.refresh_token === 'string' ? data.refresh_token : undefined,
tokenType: typeof data.token_type === 'string' ? data.token_type : undefined,
expiresAt:
typeof data.expires_in === 'number'
? new Date(Date.now() + data.expires_in * 1000)
: undefined,
scope: parseScope(data.scope),
idToken: typeof data.id_token === 'string' ? data.id_token : undefined,View on GitHub (pinned to 9696913134)
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
Example fix
// before
let tokens = await exchangeOAuthTokens({ tokenEndpoint: 'https://provider.example.com/api/v2/tokens', ... })
// after — use the actual OAuth2 token endpoint
let tokens = await exchangeOAuthTokens({ tokenEndpoint: 'https://provider.example.com/oauth/token', ... }) Defensive patterns
Strategy: try-catch
Type guard
function isJsonObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
} Try / catch
try {
let { tokens } = await provider.handleCallback(request)
} catch (error) {
if (error instanceof Error && error.message === 'Expected OAuth provider to return a JSON object.') {
// provider returned malformed token payload — inspect provider config/endpoint
}
throw error
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/7c8f20a5e8f292ee.
Report an issue: GitHub.