Skip to content

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

FieldRequiredDescription
errorYesStandard OAuth error code
error_descriptionNoHuman-readable description
error_uriNoLink to error documentation
stateConditionalState 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 CodeDescriptionCommon Causes
invalid_requestRequest is missing required parametersMissing client_id, redirect_uri, or response_type
unauthorized_clientClient not authorized for this grant typeApplication suspended or wrong grant type
access_deniedUser denied authorizationUser clicked "Deny" or failed authentication
unsupported_response_typeInvalid response typeOnly code is supported
invalid_scopeRequested scope is invalidScope doesn't exist or not allowed
server_errorServer encountered an errorTemporary server issue
temporarily_unavailableServer temporarily unavailableMaintenance 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 CodeHTTP StatusDescriptionCommon Causes
invalid_request400Malformed requestMissing required parameters
invalid_client401Client authentication failedWrong client secret or ID
invalid_grant400Invalid authorization grantExpired or already used code
unauthorized_client400Client not authorizedWrong grant type for client
unsupported_grant_type400Grant type not supportedOnly authorization_code and refresh_token
invalid_scope400Invalid scope requestedScope 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