Claude OAuth Error: Request Failed with Status Code 500 - How to Fix It
Claude OAuth Error: Request Failed with Status Code 500
If you are hitting "request failed with status code 500" when authenticating with Claude's OAuth flow, you are dealing with a server-side error in the token exchange or refresh process. This is not a client configuration problem in most cases, but there are several things on your end that can trigger it. Here is how to diagnose and fix it.
What the Error Means
A 500 status code means the server encountered an unexpected condition. In the context of Claude OAuth, this typically happens at one of three points in the authentication flow:
- Authorization code exchange - your app sends the auth code to get tokens, but the server rejects it
- Token refresh - your stored refresh token is expired or revoked, and the refresh request fails
- API request with a stale token - the access token is valid but the server-side session is broken
The error message "request failed with status code 500" is generic. It comes from the HTTP client (usually Axios) wrapping the server response. The actual root cause is in the response body or server logs.
Common Causes and Fixes
| Cause | Symptoms | Fix | |-------|----------|-----| | Expired authorization code | 500 on initial token exchange | Auth codes expire in 60 seconds. Reduce latency between redirect and token request | | Revoked refresh token | 500 on token refresh after working previously | Re-authenticate the user from scratch. Clear stored tokens | | Mismatched redirect URI | 500 on token exchange | The redirect_uri in the token request must exactly match the one used in the authorization request, including trailing slashes | | Invalid client credentials | 500 on any token operation | Verify client_id and client_secret match your OAuth app configuration | | Clock skew | Intermittent 500 errors | Sync your server clock with NTP. JWT validation fails with clock drift over 30 seconds | | Rate limiting on auth endpoints | 500 under load | Implement exponential backoff on token refresh. Cache tokens instead of requesting new ones per request | | Anthropic API outage | 500 for all users simultaneously | Check status.anthropic.com and wait for resolution |
Debugging Step by Step
1. Inspect the Full Response
Most developers only see the Axios error message. The actual error details are in the response body.
try {
const response = await axios.post('https://api.anthropic.com/oauth/token', {
grant_type: 'authorization_code',
code: authCode,
redirect_uri: redirectUri,
client_id: clientId,
client_secret: clientSecret,
});
} catch (error) {
if (axios.isAxiosError(error)) {
// Log the FULL response, not just the message
console.error('Status:', error.response?.status);
console.error('Body:', JSON.stringify(error.response?.data, null, 2));
console.error('Headers:', error.response?.headers);
}
}
The response body usually contains an error field with a machine-readable code and an error_description with a human-readable explanation.
2. Verify Your OAuth Configuration
Check these values against your OAuth app settings:
# Test the token endpoint directly with curl
curl -X POST https://api.anthropic.com/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "YOUR_AUTH_CODE",
"redirect_uri": "http://localhost:3000/callback",
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}' -v 2>&1
Pay attention to the response headers. A Retry-After header indicates rate limiting. A X-Request-Id header is useful for support tickets.
3. Check Token Storage
If the error happens on token refresh rather than initial auth, your token storage is the likely culprit.
// Bad: storing tokens in memory only
let accessToken = response.data.access_token;
// Better: persist tokens with metadata
interface StoredToken {
access_token: string;
refresh_token: string;
expires_at: number; // Unix timestamp
scope: string;
}
function isTokenExpired(token: StoredToken): boolean {
// Add 30-second buffer for clock skew
return Date.now() / 1000 > token.expires_at - 30;
}
4. Implement Proper Token Refresh
A robust token refresh handles the 500 case gracefully:
async function getValidToken(stored: StoredToken): Promise<string> {
if (!isTokenExpired(stored)) {
return stored.access_token;
}
try {
const response = await axios.post(
'https://api.anthropic.com/oauth/token',
{
grant_type: 'refresh_token',
refresh_token: stored.refresh_token,
client_id: process.env.CLAUDE_CLIENT_ID,
client_secret: process.env.CLAUDE_CLIENT_SECRET,
}
);
// Update stored tokens
const newToken: StoredToken = {
access_token: response.data.access_token,
refresh_token: response.data.refresh_token ?? stored.refresh_token,
expires_at: Math.floor(Date.now() / 1000) + response.data.expires_in,
scope: response.data.scope,
};
await saveToken(newToken);
return newToken.access_token;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 500) {
// Server error on refresh - token may be revoked
// Force re-authentication
await clearStoredTokens();
throw new Error('OAuth session expired. Please re-authenticate.');
}
throw error;
}
}
Token Refresh Race Conditions
In applications with concurrent requests, multiple threads may try to refresh the token simultaneously. This causes duplicate refresh requests, and the second one fails with a 500 because the first refresh already invalidated the old refresh token.
// Mutex-based token refresh to prevent race conditions
let refreshPromise: Promise<string> | null = null;
async function getTokenWithMutex(stored: StoredToken): Promise<string> {
if (!isTokenExpired(stored)) {
return stored.access_token;
}
// If a refresh is already in progress, wait for it
if (refreshPromise) {
return refreshPromise;
}
refreshPromise = refreshToken(stored).finally(() => {
refreshPromise = null;
});
return refreshPromise;
}
Environment-Specific Issues
Local Development
When running locally, the most common cause of 500 errors is a redirect URI mismatch. Your OAuth app might be configured for https://localhost:3000/callback but your dev server uses http://localhost:3000/callback. The protocol matters.
# Check what your dev server is actually serving
curl -I http://localhost:3000/callback
# vs
curl -I https://localhost:3000/callback
Production with Proxies
If your production app sits behind a reverse proxy (nginx, Cloudflare, AWS ALB), the proxy might modify headers or strip the request body before it reaches the OAuth endpoint. Check that:
- The
Content-Type: application/jsonheader is preserved - The request body is not truncated by a body size limit
- The proxy is not caching POST requests to the token endpoint
Docker and Containerized Environments
DNS resolution inside containers can cause intermittent 500 errors if the container cannot resolve api.anthropic.com. Test with:
# Inside the container
nslookup api.anthropic.com
curl -v https://api.anthropic.com/oauth/token
Retry Strategy
Not all 500 errors should be retried. Here is a decision framework:
async function handleOAuthError(error: AxiosError): Promise<void> {
const status = error.response?.status;
const body = error.response?.data as Record<string, string> | undefined;
if (status === 500) {
const errorCode = body?.error;
switch (errorCode) {
case 'invalid_grant':
// Auth code or refresh token is invalid - don't retry
await clearStoredTokens();
throw new Error('Session expired. Re-authenticate.');
case 'server_error':
// Transient server issue - retry with backoff
await retryWithBackoff(() => refreshToken());
break;
default:
// Unknown - log and force re-auth
console.error('Unknown OAuth 500:', body);
await clearStoredTokens();
throw new Error('OAuth error. Re-authenticate.');
}
}
}
async function retryWithBackoff(
fn: () => Promise<void>,
maxRetries = 3
): Promise<void> {
for (let i = 0; i < maxRetries; i++) {
try {
await fn();
return;
} catch {
if (i === maxRetries - 1) throw new Error('Max retries exceeded');
await new Promise((r) => setTimeout(r, Math.pow(2, i) * 1000));
}
}
}
When to Contact Anthropic Support
If you have verified your configuration, checked for token race conditions, confirmed your redirect URIs match, and the 500 persists:
- Note the
X-Request-Idfrom the error response headers - Check status.anthropic.com for ongoing incidents
- File a support ticket with the request ID, your client_id (not the secret), the timestamp, and the full error response body
Server-side 500 errors that persist across multiple users and configurations are typically resolved within hours during an incident.
Preventing Future 500 Errors
- Validate redirect URIs at startup. Compare your configured redirect URI against the OAuth app settings before the first auth request.
- Refresh tokens proactively. Do not wait for a 401 or 500 to refresh. Check
expires_atbefore each API call and refresh with a 30-second buffer. - Use a single token refresh path. Prevent concurrent refresh requests with a mutex or promise deduplication.
- Monitor token operations. Log every token exchange and refresh with the response status and timing. A spike in 500s on refresh usually means a deployment changed the client secret.
- Store tokens securely. Use encrypted storage or a secrets manager. Never log access tokens or refresh tokens in plaintext.
The most common fix is the simplest: clear your stored tokens and re-authenticate. If that resolves it, the root cause was a stale or revoked refresh token. If it does not, work through the debugging steps above to identify whether the issue is in your configuration, your infrastructure, or on Anthropic's side.
Fazm is an open source macOS AI agent that automates complex desktop workflows. Open source on GitHub.