Skip to content

Error Handling Guide

Table of Contents

Overview

The Engagifii Identity API uses standard OAuth 2.0 and OpenID Connect error responses. Understanding these error patterns helps build robust applications with proper error handling and recovery mechanisms.

Error Categories

  1. Protocol Errors: OAuth/OIDC specification violations
  2. Authentication Errors: Invalid credentials or tokens
  3. Authorization Errors: Insufficient permissions
  4. Validation Errors: Invalid request parameters
  5. Server Errors: Infrastructure or service issues

Error Response Format

OAuth 2.0 Standard Format

All OAuth endpoints return errors in this structure:

json
{
  "error": "error_code",
  "error_description": "Human-readable description of the error",
  "error_uri": "https://docs.engagifii.com/errors/error_code"
}

Authorization Endpoint Errors

Errors from /connect/authorize are returned as URL parameters:

https://app.example.com/callback?
  error=access_denied&
  error_description=User denied the authorization request&
  state=abc123

Token Endpoint Errors

Errors from /connect/token are returned as JSON:

http
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
  "error": "invalid_grant",
  "error_description": "The authorization code has expired"
}

API Endpoint Errors

Protected resource errors use Bearer authentication scheme:

http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", 
  error_description="The access token has expired"

OAuth 2.0 Error Codes

Authorization Errors

Error CodeDescriptionCommon CausesResolution
invalid_requestRequest missing required parameterMissing client_id, redirect_uriInclude all required parameters
unauthorized_clientClient not authorized for grant typeWrong flow for client typeUse appropriate grant type
access_deniedUser denied authorizationUser clicked "Deny"Handle gracefully in UI
unsupported_response_typeResponse type not supportedInvalid response_type valueUse code for auth code flow
invalid_scopeInvalid or unknown scopeRequesting non-existent scopeUse only configured scopes
server_errorServer encountered errorInternal server issueRetry with exponential backoff
temporarily_unavailableServer temporarily overloadedRate limiting or maintenanceRetry after delay

Token Endpoint Errors

Error CodeDescriptionCommon CausesResolution
invalid_requestMalformed requestMissing or duplicate parametersCheck request format
invalid_clientClient authentication failedWrong client_id or secretVerify credentials
invalid_grantInvalid authorization grantExpired or revoked code/tokenRequest new authorization
unauthorized_clientClient not authorizedGrant type not allowedCheck client configuration
unsupported_grant_typeGrant type not supportedInvalid grant_type valueUse supported grant type
invalid_scopeScope exceeds grantRequesting unauthorized scopeReduce scope request

HTTP Status Codes

Success Codes

StatusMeaningUsage
200 OKRequest succeededToken response, UserInfo
201 CreatedResource createdRegistration endpoints
204 No ContentSuccess, no bodyToken revocation
302 FoundRedirectAuthorization flow

Client Error Codes

StatusMeaningCommon Scenarios
400 Bad RequestInvalid request syntaxMalformed parameters
401 UnauthorizedAuthentication requiredMissing or invalid token
403 ForbiddenAccess deniedInsufficient permissions
404 Not FoundResource not foundInvalid endpoint
405 Method Not AllowedWrong HTTP methodGET instead of POST
429 Too Many RequestsRate limit exceededToo many requests

Server Error Codes

StatusMeaningResponse Strategy
500 Internal Server ErrorServer errorRetry with backoff
502 Bad GatewayGateway errorRetry with backoff
503 Service UnavailableService downCheck status, retry
504 Gateway TimeoutRequest timeoutRetry with backoff

Common Error Scenarios

Scenario 1: Expired Authorization Code

Error Response:

json
{
  "error": "invalid_grant",
  "error_description": "Authorization code has expired"
}

Cause: Authorization codes expire after 60 seconds

Solution:

javascript
// Restart authorization flow
function handleExpiredCode() {
  // Clear any stored state
  sessionStorage.removeItem('auth_state');
  
  // Redirect to authorization endpoint
  window.location.href = buildAuthorizationUrl();
}

Scenario 2: Invalid Client Credentials

Error Response:

json
{
  "error": "invalid_client",
  "error_description": "Client authentication failed"
}

Cause: Wrong client_id or client_secret

Solution:

javascript
// Verify credentials before request
async function authenticateClient() {
  const credentials = {
    client_id: process.env.CLIENT_ID,
    client_secret: process.env.CLIENT_SECRET
  };
  
  if (!credentials.client_id || !credentials.client_secret) {
    throw new Error('Missing client credentials');
  }
  
  return credentials;
}

Scenario 3: Expired Access Token

Error Response:

http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="Token expired"

Cause: Access token lifetime exceeded

Solution:

javascript
async function makeAuthenticatedRequest(url, options = {}) {
  let response = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${getAccessToken()}`
    }
  });
  
  if (response.status === 401) {
    // Try to refresh token
    const newToken = await refreshAccessToken();
    
    if (newToken) {
      // Retry with new token
      response = await fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          'Authorization': `Bearer ${newToken}`
        }
      });
    } else {
      // Re-authenticate user
      redirectToLogin();
    }
  }
  
  return response;
}

Scenario 4: Invalid Redirect URI

Error Response:

json
{
  "error": "invalid_request",
  "error_description": "Invalid redirect_uri"
}

Cause: Redirect URI doesn't match registered value

Solution:

javascript
// Use exact registered URI
const REDIRECT_URI = process.env.NODE_ENV === 'production' 
  ? 'https://app.example.com/callback'
  : 'http://localhost:3000/callback';

// Ensure exact match (including trailing slash)
function normalizeUri(uri) {
  return uri.replace(/\/+$/, ''); // Remove trailing slashes
}

Scenario 5: Rate Limiting

Error Response:

http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1672531200

Cause: Exceeded rate limit

Solution:

javascript
class RateLimitHandler {
  async executeWithRetry(fn, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
      try {
        const result = await fn();
        
        if (result.status === 429) {
          const retryAfter = result.headers.get('Retry-After') || 60;
          const delay = parseInt(retryAfter) * 1000;
          
          console.log(`Rate limited. Retrying after ${retryAfter} seconds...`);
          await this.sleep(delay);
          continue;
        }
        
        return result;
      } catch (error) {
        if (i === maxRetries - 1) throw error;
        
        // Exponential backoff
        const delay = Math.min(1000 * Math.pow(2, i), 10000);
        await this.sleep(delay);
      }
    }
  }
  
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

Error Recovery Strategies

Automatic Token Refresh

javascript
class TokenManager {
  constructor() {
    this.tokens = null;
    this.refreshPromise = null;
  }
  
  async getValidToken() {
    // Check if token is still valid
    if (this.isTokenValid()) {
      return this.tokens.access_token;
    }
    
    // Prevent concurrent refresh requests
    if (this.refreshPromise) {
      return this.refreshPromise;
    }
    
    // Refresh token
    this.refreshPromise = this.refreshToken()
      .finally(() => {
        this.refreshPromise = null;
      });
    
    return this.refreshPromise;
  }
  
  isTokenValid() {
    if (!this.tokens) return false;
    
    const now = Date.now() / 1000;
    const expiry = this.tokens.expires_at;
    
    // Refresh 5 minutes before expiry
    return now < (expiry - 300);
  }
  
  async refreshToken() {
    if (!this.tokens?.refresh_token) {
      throw new Error('No refresh token available');
    }
    
    try {
      const response = await fetch('/connect/token', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: new URLSearchParams({
          grant_type: 'refresh_token',
          refresh_token: this.tokens.refresh_token,
          client_id: CLIENT_ID,
          client_secret: CLIENT_SECRET
        })
      });
      
      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error_description || 'Token refresh failed');
      }
      
      const newTokens = await response.json();
      this.tokens = {
        ...newTokens,
        expires_at: Date.now() / 1000 + newTokens.expires_in
      };
      
      return this.tokens.access_token;
    } catch (error) {
      // Clear tokens on refresh failure
      this.tokens = null;
      throw error;
    }
  }
}

Graceful Degradation

javascript
class AuthService {
  async getUserInfo() {
    try {
      const response = await fetch('/connect/userinfo', {
        headers: {
          'Authorization': `Bearer ${this.getAccessToken()}`
        }
      });
      
      if (!response.ok) {
        throw new Error('Failed to get user info');
      }
      
      return await response.json();
    } catch (error) {
      console.error('UserInfo error:', error);
      
      // Fall back to cached data
      const cachedUser = this.getCachedUserInfo();
      if (cachedUser) {
        console.log('Using cached user info');
        return cachedUser;
      }
      
      // Fall back to ID token claims
      const idToken = this.parseIdToken();
      if (idToken) {
        console.log('Using ID token claims');
        return {
          sub: idToken.sub,
          name: idToken.name,
          email: idToken.email
        };
      }
      
      throw error;
    }
  }
}

Circuit Breaker Pattern

javascript
class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failureCount = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = 'CLOSED';
    this.nextAttempt = Date.now();
  }
  
  async execute(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('Circuit breaker is OPEN');
      }
      this.state = 'HALF_OPEN';
    }
    
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  onSuccess() {
    this.failureCount = 0;
    this.state = 'CLOSED';
  }
  
  onFailure() {
    this.failureCount++;
    if (this.failureCount >= this.threshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.timeout;
      console.log(`Circuit breaker opened until ${new Date(this.nextAttempt)}`);
    }
  }
}

// Usage
const breaker = new CircuitBreaker();
const token = await breaker.execute(() => getAccessToken());

Debugging Techniques

Request/Response Logging

javascript
// Development logging middleware
function logRequests(fetch) {
  return async (url, options = {}) => {
    console.group(`🔵 ${options.method || 'GET'} ${url}`);
    console.log('Request:', {
      headers: options.headers,
      body: options.body
    });
    
    try {
      const response = await fetch(url, options);
      const clone = response.clone();
      
      console.log('Response:', {
        status: response.status,
        headers: Object.fromEntries(response.headers.entries())
      });
      
      if (response.headers.get('content-type')?.includes('json')) {
        console.log('Body:', await clone.json());
      }
      
      console.groupEnd();
      return response;
    } catch (error) {
      console.error('Error:', error);
      console.groupEnd();
      throw error;
    }
  };
}

// Use in development
if (process.env.NODE_ENV === 'development') {
  window.fetch = logRequests(window.fetch);
}

Token Inspection

javascript
function inspectToken(token) {
  try {
    const parts = token.split('.');
    if (parts.length !== 3) {
      throw new Error('Invalid JWT format');
    }
    
    const header = JSON.parse(atob(parts[0]));
    const payload = JSON.parse(atob(parts[1]));
    
    const now = Date.now() / 1000;
    const expiry = new Date(payload.exp * 1000);
    const isExpired = now > payload.exp;
    
    console.table({
      'Token Type': header.typ,
      'Algorithm': header.alg,
      'Key ID': header.kid,
      'Issuer': payload.iss,
      'Subject': payload.sub,
      'Client ID': payload.client_id,
      'Scopes': payload.scope,
      'Issued At': new Date(payload.iat * 1000).toISOString(),
      'Expires At': expiry.toISOString(),
      'Is Expired': isExpired,
      'Time Until Expiry': isExpired ? 'Expired' : `${Math.round((payload.exp - now) / 60)} minutes`
    });
    
    return { header, payload, isExpired };
  } catch (error) {
    console.error('Token inspection failed:', error);
    return null;
  }
}

Error Tracking

javascript
class ErrorTracker {
  constructor() {
    this.errors = [];
    this.maxErrors = 100;
  }
  
  track(error, context = {}) {
    const errorEntry = {
      timestamp: new Date().toISOString(),
      message: error.message || error.error_description || 'Unknown error',
      code: error.error || error.code,
      status: error.status,
      context,
      stack: error.stack,
      ...error
    };
    
    this.errors.unshift(errorEntry);
    this.errors = this.errors.slice(0, this.maxErrors);
    
    // Log to console in development
    if (process.env.NODE_ENV === 'development') {
      console.error('Auth Error:', errorEntry);
    }
    
    // Send to monitoring service in production
    if (process.env.NODE_ENV === 'production') {
      this.sendToMonitoring(errorEntry);
    }
    
    return errorEntry;
  }
  
  getRecentErrors(count = 10) {
    return this.errors.slice(0, count);
  }
  
  getErrorsByType(errorCode) {
    return this.errors.filter(e => e.code === errorCode);
  }
  
  clearErrors() {
    this.errors = [];
  }
  
  sendToMonitoring(error) {
    // Integrate with error monitoring service
    // e.g., Sentry, Rollbar, etc.
  }
}

const errorTracker = new ErrorTracker();

Best Practices

1. Comprehensive Error Handling

javascript
class AuthClient {
  async request(endpoint, options = {}) {
    try {
      const response = await fetch(endpoint, options);
      
      // Check for HTTP errors
      if (!response.ok) {
        const error = await this.parseError(response);
        throw new AuthError(error);
      }
      
      return await response.json();
    } catch (error) {
      // Network errors
      if (error instanceof TypeError) {
        throw new NetworkError('Network request failed', error);
      }
      
      // Auth errors
      if (error instanceof AuthError) {
        this.handleAuthError(error);
        throw error;
      }
      
      // Unexpected errors
      throw new UnexpectedError('An unexpected error occurred', error);
    }
  }
  
  async parseError(response) {
    try {
      const contentType = response.headers.get('content-type');
      
      if (contentType?.includes('application/json')) {
        return await response.json();
      }
      
      return {
        error: 'server_error',
        error_description: await response.text() || response.statusText,
        status: response.status
      };
    } catch {
      return {
        error: 'parse_error',
        error_description: 'Failed to parse error response',
        status: response.status
      };
    }
  }
  
  handleAuthError(error) {
    switch (error.code) {
      case 'invalid_token':
      case 'token_expired':
        this.clearTokens();
        this.redirectToLogin();
        break;
      
      case 'insufficient_scope':
        this.requestAdditionalScopes();
        break;
      
      case 'invalid_client':
        console.error('Client configuration error');
        break;
    }
  }
}

2. User-Friendly Error Messages

javascript
function getUserMessage(error) {
  const errorMessages = {
    'invalid_grant': 'Your session has expired. Please log in again.',
    'access_denied': 'You denied the authorization request.',
    'invalid_client': 'There was a configuration error. Please contact support.',
    'server_error': 'We're experiencing technical difficulties. Please try again later.',
    'network_error': 'Connection failed. Please check your internet connection.',
    'rate_limited': 'Too many requests. Please wait a moment and try again.'
  };
  
  return errorMessages[error.code] || error.error_description || 'An error occurred. Please try again.';
}

3. Error Recovery UI

javascript
// React example
function ErrorBoundary({ children }) {
  const [error, setError] = useState(null);
  
  const handleError = (error) => {
    setError(error);
  };
  
  const retry = () => {
    setError(null);
    window.location.reload();
  };
  
  if (error) {
    return (
      <div className="error-container">
        <h2>Something went wrong</h2>
        <p>{getUserMessage(error)}</p>
        <button onClick={retry}>Retry</button>
        {error.code === 'invalid_grant' && (
          <button onClick={() => redirectToLogin()}>Log In Again</button>
        )}
      </div>
    );
  }
  
  return children;
}

4. Monitoring and Alerting

javascript
class AuthMonitor {
  constructor() {
    this.metrics = {
      totalRequests: 0,
      failedRequests: 0,
      tokenRefreshes: 0,
      errors: {}
    };
  }
  
  recordRequest(success, errorCode = null) {
    this.metrics.totalRequests++;
    
    if (!success) {
      this.metrics.failedRequests++;
      this.metrics.errors[errorCode] = (this.metrics.errors[errorCode] || 0) + 1;
      
      // Alert on high failure rate
      const failureRate = this.metrics.failedRequests / this.metrics.totalRequests;
      if (failureRate > 0.1) {
        this.sendAlert(`High auth failure rate: ${(failureRate * 100).toFixed(1)}%`);
      }
    }
  }
  
  recordTokenRefresh(success) {
    this.metrics.tokenRefreshes++;
    if (!success) {
      this.sendAlert('Token refresh failed');
    }
  }
  
  getMetrics() {
    return {
      ...this.metrics,
      successRate: ((this.metrics.totalRequests - this.metrics.failedRequests) / this.metrics.totalRequests * 100).toFixed(1) + '%'
    };
  }
  
  sendAlert(message) {
    console.error(`🚨 Auth Alert: ${message}`);
    // Send to monitoring service
  }
}

Next Steps: Review the Quick Reference for a concise summary of common operations and patterns.