Error Handling
This guide covers OAuth error responses, common error scenarios, and best practices for handling errors in your WeTrials OAuth integration.
Error Response Format
All OAuth errors follow the standard OAuth 2.0 error response format:
json
{
"error": "error_code",
"error_description": "Human-readable error description",
"error_uri": "https://developers.wetrials.com/oauth/errors#error_code"
}Error Response Fields
| Field | Required | Description |
|---|---|---|
error | Yes | Standard OAuth error code |
error_description | No | Human-readable description |
error_uri | No | Link to error documentation |
state | Conditional | State parameter if provided in request |
OAuth Error Codes
Authorization Endpoint Errors
Errors from /v1/oauth/authorize are returned as query parameters to the redirect URI:
https://yourapp.com/callback?error=access_denied&error_description=User+denied+access&state=xyz123| Error Code | Description | Common Causes |
|---|---|---|
invalid_request | Request is missing required parameters | Missing client_id, redirect_uri, or response_type |
unauthorized_client | Client not authorized for this grant type | Application suspended or wrong grant type |
access_denied | User denied authorization | User clicked "Deny" or failed authentication |
unsupported_response_type | Invalid response type | Only code is supported |
invalid_scope | Requested scope is invalid | Scope doesn't exist or not allowed |
server_error | Server encountered an error | Temporary server issue |
temporarily_unavailable | Server temporarily unavailable | Maintenance or high load |
Token Endpoint Errors
Errors from /v1/oauth/token are returned as JSON with appropriate HTTP status codes:
http
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": "invalid_grant",
"error_description": "Authorization code has expired"
}| Error Code | HTTP Status | Description | Common Causes |
|---|---|---|---|
invalid_request | 400 | Malformed request | Missing required parameters |
invalid_client | 401 | Client authentication failed | Wrong client secret or ID |
invalid_grant | 400 | Invalid authorization grant | Expired or already used code |
unauthorized_client | 400 | Client not authorized | Wrong grant type for client |
unsupported_grant_type | 400 | Grant type not supported | Only authorization_code and refresh_token |
invalid_scope | 400 | Invalid scope requested | Scope not granted or doesn't exist |
Common Error Scenarios
1. Invalid Client Credentials
Error Response:
json
{
"error": "invalid_client",
"error_description": "Client authentication failed"
}Causes:
- Incorrect client ID or secret
- Client secret expired or rotated
- Application not activated
Solution:
javascript
// Verify credentials
const config = {
clientId: process.env.WETRIALS_CLIENT_ID,
clientSecret: process.env.WETRIALS_CLIENT_SECRET,
};
// Check for environment variables
if (!config.clientId || !config.clientSecret) {
throw new Error('OAuth credentials not configured');
}2. Redirect URI Mismatch
Error Response:
json
{
"error": "invalid_request",
"error_description": "Redirect URI mismatch"
}Causes:
- Redirect URI doesn't match registered URI exactly
- Using HTTP instead of HTTPS
- Including or excluding trailing slashes
Solution:
javascript
// Ensure exact match
const REDIRECT_URI = 'https://yourapp.com/callback'; // Exact match required
// Don't dynamically generate
// ❌ WRONG
const redirectUri = `${window.location.origin}/callback`;
// ✅ CORRECT
const redirectUri = process.env.REDIRECT_URI;3. Expired Authorization Code
Error Response:
json
{
"error": "invalid_grant",
"error_description": "Authorization code has expired"
}Causes:
- Code not exchanged within 10 minutes
- Code already used (codes are single-use)
Solution:
javascript
// Exchange code immediately
async function handleCallback(code) {
// Exchange within 10 minutes
try {
const tokens = await exchangeCodeForTokens(code);
return tokens;
} catch (error) {
if (error.error === 'invalid_grant') {
// Restart OAuth flow
redirectToAuthorization();
}
}
}4. Invalid PKCE Verification
Error Response:
json
{
"error": "invalid_grant",
"error_description": "Invalid code verifier"
}Causes:
- Code verifier doesn't match challenge
- Verifier not provided when required
- Wrong hashing method
Solution:
javascript
// Ensure proper PKCE implementation
class PKCEManager {
generateVerifier() {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return this.base64URLEncode(array);
}
async generateChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
return this.base64URLEncode(new Uint8Array(hash));
}
base64URLEncode(buffer) {
return btoa(String.fromCharCode(...buffer))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
}5. Expired Access Token
Error Response from API:
json
{
"error": "invalid_token",
"error_description": "Access token has expired"
}Solution:
javascript
class TokenManager {
async makeRequest(url, options) {
let response = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${this.accessToken}`,
},
});
// Handle token expiration
if (response.status === 401) {
const error = await response.json();
if (error.error === 'invalid_token') {
// Refresh token
await this.refreshAccessToken();
// Retry request
response = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${this.accessToken}`,
},
});
}
}
return response;
}
async refreshAccessToken() {
const response = await fetch('/oauth/refresh', {
method: 'POST',
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
client_id: this.clientId,
}),
});
if (!response.ok) {
// Refresh failed - need new authorization
this.redirectToLogin();
throw new Error('Session expired');
}
const tokens = await response.json();
this.updateTokens(tokens);
}
}Error Handling Best Practices
1. Implement Comprehensive Error Handling
javascript
class OAuthErrorHandler {
handleError(error) {
console.error('OAuth Error:', error);
switch (error.error) {
case 'invalid_client':
return this.handleInvalidClient(error);
case 'invalid_grant':
return this.handleInvalidGrant(error);
case 'access_denied':
return this.handleAccessDenied(error);
case 'invalid_scope':
return this.handleInvalidScope(error);
case 'server_error':
case 'temporarily_unavailable':
return this.handleServerError(error);
default:
return this.handleUnknownError(error);
}
}
handleInvalidClient(error) {
// Log for debugging
console.error('Client configuration error:', error);
// Notify administrators
this.notifyAdmins('OAuth client configuration error', error);
// Show user-friendly message
return {
userMessage: 'Authentication configuration error. Please contact support.',
shouldRetry: false,
};
}
handleInvalidGrant(error) {
// Clear stored tokens
this.clearStoredTokens();
// Restart OAuth flow
return {
userMessage: 'Your session has expired. Please log in again.',
shouldRetry: true,
action: () => this.restartOAuthFlow(),
};
}
handleAccessDenied(error) {
return {
userMessage: 'You denied access to the application.',
shouldRetry: true,
action: () => (window.location.href = '/'),
};
}
handleServerError(error) {
return {
userMessage: 'Temporary server issue. Please try again.',
shouldRetry: true,
retryAfter: 5000, // 5 seconds
};
}
}2. User-Friendly Error Messages
javascript
// Map technical errors to user-friendly messages
const ERROR_MESSAGES = {
invalid_client: 'Configuration error. Please contact support.',
invalid_grant: 'Your session has expired. Please log in again.',
access_denied: 'You canceled the authorization.',
invalid_scope: 'Requested permissions are not available.',
server_error: 'Server error. Please try again later.',
temporarily_unavailable: 'Service temporarily unavailable.',
invalid_token: 'Your session has expired.',
insufficient_scope: "You don't have permission for this action.",
};
function getUserMessage(error) {
return ERROR_MESSAGES[error.error] || 'An unexpected error occurred.';
}3. Implement Retry Logic
javascript
class RetryableOAuth {
async executeWithRetry(fn, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
// Don't retry certain errors
if (!this.isRetryable(error)) {
throw error;
}
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
await this.sleep(delay);
console.log(`Retry attempt ${attempt} after ${delay}ms`);
}
}
throw lastError;
}
isRetryable(error) {
const retryableErrors = ['server_error', 'temporarily_unavailable', 'timeout'];
return retryableErrors.includes(error.error);
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}4. Logging and Monitoring
javascript
class OAuthLogger {
logError(error, context) {
const errorLog = {
timestamp: new Date().toISOString(),
error: error.error,
description: error.error_description,
context: context,
userAgent: navigator.userAgent,
url: window.location.href,
};
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.error('OAuth Error:', errorLog);
}
// Send to error tracking service
this.sendToErrorTracking(errorLog);
// Store in local storage for debugging
this.storeErrorLocally(errorLog);
}
sendToErrorTracking(errorLog) {
// Send to Sentry, LogRocket, etc.
if (window.Sentry) {
window.Sentry.captureException(new Error(errorLog.description), {
extra: errorLog,
});
}
}
storeErrorLocally(errorLog) {
const errors = JSON.parse(localStorage.getItem('oauth_errors') || '[]');
errors.push(errorLog);
// Keep only last 10 errors
if (errors.length > 10) {
errors.shift();
}
localStorage.setItem('oauth_errors', JSON.stringify(errors));
}
}Rate Limiting Errors
Handling Rate Limits
javascript
class RateLimitHandler {
async handleRateLimit(response) {
const retryAfter = response.headers.get('Retry-After');
const rateLimitRemaining = response.headers.get('X-RateLimit-Remaining');
const rateLimitReset = response.headers.get('X-RateLimit-Reset');
if (response.status === 429) {
const waitTime = retryAfter ? parseInt(retryAfter) * 1000 : this.calculateWaitTime(rateLimitReset);
console.warn(`Rate limited. Waiting ${waitTime}ms before retry`);
await this.sleep(waitTime);
return true; // Retry the request
}
// Proactive rate limit management
if (rateLimitRemaining && parseInt(rateLimitRemaining) < 10) {
console.warn(`Approaching rate limit: ${rateLimitRemaining} remaining`);
// Slow down requests
await this.sleep(1000);
}
return false;
}
calculateWaitTime(resetTime) {
if (!resetTime) return 60000; // Default 1 minute
const reset = new Date(parseInt(resetTime) * 1000);
const now = new Date();
return Math.max(reset - now, 0);
}
}Testing Error Handling
Unit Tests
javascript
// Jest example
describe('OAuth Error Handler', () => {
test('handles invalid_client error', () => {
const error = {
error: 'invalid_client',
error_description: 'Client authentication failed',
};
const handler = new OAuthErrorHandler();
const result = handler.handleError(error);
expect(result.shouldRetry).toBe(false);
expect(result.userMessage).toContain('configuration error');
});
test('handles expired token with retry', async () => {
const mockRefresh = jest.fn().mockResolvedValue({
access_token: 'new_token',
refresh_token: 'new_refresh',
});
const manager = new TokenManager();
manager.refreshAccessToken = mockRefresh;
await manager.handleExpiredToken();
expect(mockRefresh).toHaveBeenCalled();
});
});Integration Tests
javascript
// Cypress example
describe('OAuth Error Flows', () => {
it('handles redirect URI mismatch', () => {
cy.visit('/login');
cy.get('[data-testid=login-button]').click();
// Intercept OAuth response
cy.intercept('GET', '/callback*', {
statusCode: 302,
headers: {
Location: '/callback?error=invalid_request&error_description=Redirect+URI+mismatch',
},
});
cy.url().should('include', 'error=invalid_request');
cy.get('[data-testid=error-message]').should('contain', 'Configuration error');
});
});Debugging OAuth Errors
Debug Mode
javascript
class OAuthDebugger {
constructor(enabled = false) {
this.enabled = enabled || process.env.NODE_ENV === 'development';
}
logRequest(endpoint, params) {
if (!this.enabled) return;
console.group(`OAuth Request: ${endpoint}`);
console.log('Parameters:', params);
console.log('Timestamp:', new Date().toISOString());
console.groupEnd();
}
logResponse(endpoint, response, data) {
if (!this.enabled) return;
console.group(`OAuth Response: ${endpoint}`);
console.log('Status:', response.status);
console.log('Headers:', Object.fromEntries(response.headers));
console.log('Data:', data);
console.groupEnd();
}
logError(error) {
if (!this.enabled) return;
console.group('OAuth Error');
console.error('Error:', error.error);
console.error('Description:', error.error_description);
console.error('Stack:', new Error().stack);
console.groupEnd();
}
}
// Usage
const debugger = new OAuthDebugger(true);Browser DevTools
javascript
// Add debug information to window for DevTools access
if (process.env.NODE_ENV === 'development') {
window.__OAUTH_DEBUG__ = {
lastError: null,
requests: [],
tokens: {
access: tokenManager.getAccessToken(),
refresh: tokenManager.getRefreshToken(),
},
config: {
clientId: oauth.clientId,
redirectUri: oauth.redirectUri,
scopes: oauth.scopes,
},
};
}Next Steps
- Security Best Practices - Secure error handling
- Authorization Code Flow - Implementation guide
- Managing Applications - Application troubleshooting
- Scopes and Permissions - Scope-related errors