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 ​