Appearance
Error Handling Guide
Table of Contents
- Overview
- Error Response Format
- OAuth 2.0 Error Codes
- HTTP Status Codes
- Common Error Scenarios
- Error Recovery Strategies
- Debugging Techniques
- Best Practices
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
- Protocol Errors: OAuth/OIDC specification violations
- Authentication Errors: Invalid credentials or tokens
- Authorization Errors: Insufficient permissions
- Validation Errors: Invalid request parameters
- 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=abc123Token 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 Code | Description | Common Causes | Resolution |
|---|---|---|---|
invalid_request | Request missing required parameter | Missing client_id, redirect_uri | Include all required parameters |
unauthorized_client | Client not authorized for grant type | Wrong flow for client type | Use appropriate grant type |
access_denied | User denied authorization | User clicked "Deny" | Handle gracefully in UI |
unsupported_response_type | Response type not supported | Invalid response_type value | Use code for auth code flow |
invalid_scope | Invalid or unknown scope | Requesting non-existent scope | Use only configured scopes |
server_error | Server encountered error | Internal server issue | Retry with exponential backoff |
temporarily_unavailable | Server temporarily overloaded | Rate limiting or maintenance | Retry after delay |
Token Endpoint Errors
| Error Code | Description | Common Causes | Resolution |
|---|---|---|---|
invalid_request | Malformed request | Missing or duplicate parameters | Check request format |
invalid_client | Client authentication failed | Wrong client_id or secret | Verify credentials |
invalid_grant | Invalid authorization grant | Expired or revoked code/token | Request new authorization |
unauthorized_client | Client not authorized | Grant type not allowed | Check client configuration |
unsupported_grant_type | Grant type not supported | Invalid grant_type value | Use supported grant type |
invalid_scope | Scope exceeds grant | Requesting unauthorized scope | Reduce scope request |
HTTP Status Codes
Success Codes
| Status | Meaning | Usage |
|---|---|---|
200 OK | Request succeeded | Token response, UserInfo |
201 Created | Resource created | Registration endpoints |
204 No Content | Success, no body | Token revocation |
302 Found | Redirect | Authorization flow |
Client Error Codes
| Status | Meaning | Common Scenarios |
|---|---|---|
400 Bad Request | Invalid request syntax | Malformed parameters |
401 Unauthorized | Authentication required | Missing or invalid token |
403 Forbidden | Access denied | Insufficient permissions |
404 Not Found | Resource not found | Invalid endpoint |
405 Method Not Allowed | Wrong HTTP method | GET instead of POST |
429 Too Many Requests | Rate limit exceeded | Too many requests |
Server Error Codes
| Status | Meaning | Response Strategy |
|---|---|---|
500 Internal Server Error | Server error | Retry with backoff |
502 Bad Gateway | Gateway error | Retry with backoff |
503 Service Unavailable | Service down | Check status, retry |
504 Gateway Timeout | Request timeout | Retry 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: 1672531200Cause: 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.
