Error Handling
This guide covers error handling patterns, standard response formats, HTTP status codes, and best practices for building resilient applications with the AuthOS API.
Overview
The AuthOS API uses standard HTTP status codes and returns consistent, structured error responses. Understanding these patterns helps you build robust error handling into your applications.
Key Principles
- Consistent Format: All errors follow the same JSON structure
- Descriptive Messages: Human-readable error messages for debugging
- Machine-Readable Codes: Structured error codes for programmatic handling
- HTTP Standards: Proper use of HTTP status codes
- Timestamp Tracking: Every error includes a timestamp for correlation
Error Response Format
All API errors return a JSON object with the following structure:
{
"error": "Human-readable error message describing what went wrong",
"error_code": "MACHINE_READABLE_ERROR_CODE",
"timestamp": "2025-01-15T10:30:00Z"
}
Fields
| Field | Type | Description |
|---|---|---|
error |
string | Human-readable description of the error |
error_code |
string | Machine-readable error code in SCREAMING_SNAKE_CASE |
timestamp |
string | ISO 8601 timestamp when the error occurred |
Example
{
"error": "Organization not found",
"error_code": "NOT_FOUND",
"timestamp": "2025-01-15T10:30:00.123Z"
}
HTTP Status Codes
The API uses standard HTTP status codes to indicate the general category of error.
2xx Success
| Code | Meaning | Description |
|---|---|---|
200 OK |
Success | Request succeeded and response contains data |
201 Created |
Resource Created | New resource created successfully |
204 No Content |
Success (No Response Body) | Request succeeded, no content to return |
4xx Client Errors
| Code | Meaning | When It Occurs |
|---|---|---|
400 Bad Request |
Invalid Request | Malformed JSON, invalid parameters, business logic violation |
401 Unauthorized |
Authentication Failed | Missing, invalid, or expired JWT/API key |
403 Forbidden |
Authorization Failed | Valid credentials but insufficient permissions |
404 Not Found |
Resource Not Found | Requested resource doesn’t exist |
409 Conflict |
Resource Conflict | Resource already exists or state conflict |
429 Too Many Requests |
Rate Limited | Exceeded rate limit for endpoint |
5xx Server Errors
| Code | Meaning | When It Occurs |
|---|---|---|
500 Internal Server Error |
Server Error | Unexpected server-side error |
503 Service Unavailable |
Service Down | API temporarily unavailable (maintenance, overload) |
Error Codes Reference
General Errors
| Error Code | HTTP Status | Description | Common Causes |
|---|---|---|---|
BAD_REQUEST |
400 | Invalid request parameters | Malformed JSON, missing required fields, invalid data types |
UNAUTHORIZED |
401 | Authentication failed | Missing token, invalid token, malformed Authorization header |
FORBIDDEN |
403 | Insufficient permissions | Valid credentials but wrong role/permissions |
NOT_FOUND |
404 | Resource not found | Invalid ID, deleted resource, wrong endpoint |
INTERNAL_SERVER_ERROR |
500 | Server error | Database error, unexpected exception |
Authentication & Authorization Errors
| Error Code | HTTP Status | Description | Resolution |
|---|---|---|---|
UNAUTHORIZED |
401 | Missing or invalid credentials | Provide valid JWT or API key |
TOKEN_EXPIRED |
401 | JWT has expired | Refresh token using /api/auth/refresh |
JWT_ERROR |
401 | Invalid JWT signature or format | Obtain new token via authentication |
FORBIDDEN |
403 | Insufficient permissions | Request access or use account with proper role |
ORGANIZATION_NOT_ACTIVE |
403 | Organization suspended or pending | Contact platform admin |
Business Logic Errors
| Error Code | HTTP Status | Description | Resolution |
|---|---|---|---|
SERVICE_LIMIT_EXCEEDED |
400 | Max services reached for org tier | Upgrade tier or delete unused services |
TEAM_LIMIT_EXCEEDED |
400 | Max team members reached | Upgrade tier or remove members |
INVITATION_EXPIRED |
400 | Invitation link expired | Request new invitation |
DEVICE_CODE_EXPIRED |
400 | Device authorization code expired | Start new device flow |
DEVICE_CODE_PENDING |
400 | User hasn’t authorized device yet | Continue polling |
Database & Infrastructure Errors
| Error Code | HTTP Status | Description | Resolution |
|---|---|---|---|
DATABASE_ERROR |
500 | Database operation failed | Retry request; contact support if persists |
OAUTH_ERROR |
500 | OAuth provider error | Check provider status; retry later |
STRIPE_ERROR |
500 | Billing system error | Check Stripe status; contact support |
AUDIT_ERROR |
500 | Audit logging failed | Request succeeded but not logged; report to support |
Common Error Scenarios
Authentication Errors
Missing Authorization Header
Request:
curl -X GET https://sso.example.com/api/user
# Missing: -H "Authorization: Bearer {jwt}"
Response (401 Unauthorized):
{
"error": "Missing or invalid Authorization header",
"error_code": "UNAUTHORIZED",
"timestamp": "2025-01-15T10:30:00Z"
}
Solution: Include Authorization: Bearer {jwt} header in request.
Expired JWT Token
Request:
curl -X GET https://sso.example.com/api/user \
-H "Authorization: Bearer {expired_jwt}"
Response (401 Unauthorized):
{
"error": "Token expired",
"error_code": "TOKEN_EXPIRED",
"timestamp": "2025-01-15T10:30:00Z"
}
Solution: Use refresh token to get new access token:
curl -X POST https://sso.example.com/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "your-refresh-token"}'
Invalid API Key
Request:
curl -X GET https://sso.example.com/api/service/users \
-H "X-API-Key: sk_invalid_key"
Response (401 Unauthorized):
{
"error": "Invalid API key",
"error_code": "UNAUTHORIZED",
"timestamp": "2025-01-15T10:30:00Z"
}
Solution: Verify API key is correct and hasn’t been revoked.
Authorization Errors
Insufficient Permissions
Request: Platform owner endpoint without platform owner role
curl -X GET https://sso.example.com/api/platform/organizations \
-H "Authorization: Bearer {non_platform_owner_jwt}"
Response (403 Forbidden):
{
"error": "Platform owner access required",
"error_code": "FORBIDDEN",
"timestamp": "2025-01-15T10:30:00Z"
}
Solution: Request platform owner access or use proper admin account.
Organization Not Active
Request: Attempting to authenticate with suspended organization
curl -X GET https://sso.example.com/auth/github?org=suspended-org&service=app
Response (403 Forbidden):
{
"error": "Organization is not active",
"error_code": "ORGANIZATION_NOT_ACTIVE",
"timestamp": "2025-01-15T10:30:00Z"
}
Solution: Contact platform administrator to reactivate organization.
Validation Errors
Missing Required Field
Request:
curl -X POST https://sso.example.com/api/service/users \
-H "X-API-Key: sk_live_abc123" \
-H "Content-Type: application/json" \
-d '{}'
Response (400 Bad Request):
{
"error": "Missing required field: email",
"error_code": "BAD_REQUEST",
"timestamp": "2025-01-15T10:30:00Z"
}
Solution: Include all required fields in request body.
Invalid Email Format
Request:
curl -X POST https://sso.example.com/api/service/users \
-H "X-API-Key: sk_live_abc123" \
-H "Content-Type: application/json" \
-d '{"email": "not-an-email"}'
Response (400 Bad Request):
{
"error": "Invalid email format",
"error_code": "BAD_REQUEST",
"timestamp": "2025-01-15T10:30:00Z"
}
Solution: Provide a valid email address.
Resource Errors
Resource Not Found
Request:
curl -X GET https://sso.example.com/api/service/users/invalid-user-id \
-H "X-API-Key: sk_live_abc123"
Response (404 Not Found):
{
"error": "User not found or has not authenticated with this service",
"error_code": "NOT_FOUND",
"timestamp": "2025-01-15T10:30:00Z"
}
Solution: Verify the resource ID and ensure user has authenticated with your service.
Resource Limit Exceeded
Request: Creating service when at tier limit
curl -X POST https://sso.example.com/api/organizations/acme-corp/services \
-H "Authorization: Bearer {jwt}" \
-d '{"name": "New Service", "slug": "new-svc"}'
Response (400 Bad Request):
{
"error": "Service limit exceeded for organization tier",
"error_code": "SERVICE_LIMIT_EXCEEDED",
"timestamp": "2025-01-15T10:30:00Z"
}
Solution: Upgrade organization tier or delete unused services.
Rate Limiting
Too Many Requests
Request: Exceeding rate limit
# 101st request within rate limit window
curl -X GET https://sso.example.com/api/service/users \
-H "X-API-Key: sk_live_abc123"
Response (429 Too Many Requests):
{
"error": "Rate limit exceeded",
"error_code": "RATE_LIMIT_EXCEEDED",
"timestamp": "2025-01-15T10:30:00Z"
}
Headers:
Retry-After: 60
Solution: Wait for rate limit reset or implement exponential backoff.
See Also: Rate Limiting Concepts for detailed rate limit policies and best practices.
Error Handling Best Practices
1. Check HTTP Status First
Always check the HTTP status code before parsing the response body:
async function makeRequest(url, options) {
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
throw new APIError(response.status, error);
}
return response.json();
}
class APIError extends Error {
constructor(status, errorBody) {
super(errorBody.error);
this.status = status;
this.code = errorBody.error_code;
this.timestamp = errorBody.timestamp;
}
}
2. Handle Token Expiration Automatically
Implement automatic token refresh on TOKEN_EXPIRED errors:
async function apiRequestWithRefresh(url, options) {
let response = await fetch(url, options);
if (response.status === 401) {
const error = await response.json();
if (error.error_code === 'TOKEN_EXPIRED') {
// Refresh token
const newTokens = await refreshAccessToken();
// Retry original request with new token
options.headers['Authorization'] = `Bearer ${newTokens.access_token}`;
response = await fetch(url, options);
}
}
return response;
}
3. Implement Exponential Backoff for Rate Limits
Use exponential backoff when encountering rate limits:
async function fetchWithBackoff(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
const backoff = Math.min(retryAfter * 1000, 2 ** i * 1000);
console.log(`Rate limited. Retrying after ${backoff}ms...`);
await new Promise(resolve => setTimeout(resolve, backoff));
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}
4. Handle Network Errors Separately
Distinguish between API errors and network failures:
async function apiRequest(url, options) {
try {
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
throw new APIError(response.status, error);
}
return response.json();
} catch (error) {
if (error instanceof APIError) {
// API returned an error response
console.error('API Error:', error.code, error.message);
throw error;
} else {
// Network error (timeout, DNS failure, etc.)
console.error('Network Error:', error.message);
throw new NetworkError(error);
}
}
}
5. Log Errors with Context
Include request context when logging errors:
async function apiRequest(url, options) {
const requestId = generateRequestId();
try {
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
console.error('API Request Failed', {
requestId,
url,
method: options.method,
status: response.status,
errorCode: error.error_code,
errorMessage: error.error,
timestamp: error.timestamp
});
throw new APIError(response.status, error);
}
return response.json();
} catch (error) {
if (!(error instanceof APIError)) {
console.error('Request Failed', {
requestId,
url,
method: options.method,
error: error.message
});
}
throw error;
}
}
6. Display User-Friendly Messages
Don’t expose raw error messages to end users:
function getUserFriendlyMessage(error) {
const friendlyMessages = {
'TOKEN_EXPIRED': 'Your session has expired. Please log in again.',
'UNAUTHORIZED': 'Authentication failed. Please log in.',
'FORBIDDEN': 'You don\'t have permission to perform this action.',
'NOT_FOUND': 'The requested resource could not be found.',
'SERVICE_LIMIT_EXCEEDED': 'You\'ve reached your service limit. Please upgrade your plan.',
'RATE_LIMIT_EXCEEDED': 'Too many requests. Please try again in a moment.',
'INTERNAL_SERVER_ERROR': 'Something went wrong. Please try again later.'
};
return friendlyMessages[error.code] || 'An unexpected error occurred.';
}
7. Validate Before Sending
Validate input client-side before making API requests:
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new ValidationError('Invalid email format');
}
}
async function createUser(email) {
// Validate first to avoid unnecessary API call
validateEmail(email);
return apiRequest('/api/service/users', {
method: 'POST',
headers: {
'X-API-Key': process.env.SSO_API_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email })
});
}
8. Handle Specific Error Codes
Implement specific handlers for different error codes:
async function handleAPIError(error) {
switch (error.code) {
case 'TOKEN_EXPIRED':
await refreshAndRetry();
break;
case 'ORGANIZATION_NOT_ACTIVE':
redirectToSuspendedPage();
break;
case 'SERVICE_LIMIT_EXCEEDED':
showUpgradeModal();
break;
case 'RATE_LIMIT_EXCEEDED':
await retryWithBackoff();
break;
default:
showGenericError(getUserFriendlyMessage(error));
}
}
Testing Error Scenarios
Unit Testing Error Handling
describe('API Error Handling', () => {
it('should refresh token on TOKEN_EXPIRED', async () => {
// Mock expired token response
fetchMock.mockResponseOnce(
JSON.stringify({
error: 'Token expired',
error_code: 'TOKEN_EXPIRED',
timestamp: new Date().toISOString()
}),
{ status: 401 }
);
// Mock successful refresh
fetchMock.mockResponseOnce(
JSON.stringify({
access_token: 'new_token',
refresh_token: 'new_refresh_token'
})
);
// Mock successful retry
fetchMock.mockResponseOnce(
JSON.stringify({ id: 'user_123' })
);
const result = await apiRequestWithRefresh('/api/user');
expect(result.id).toBe('user_123');
});
it('should handle rate limiting with backoff', async () => {
// Mock rate limit response
fetchMock.mockResponseOnce('', {
status: 429,
headers: { 'Retry-After': '2' }
});
// Mock successful retry
fetchMock.mockResponseOnce(
JSON.stringify({ users: [] })
);
const result = await fetchWithBackoff('/api/service/users');
expect(result.users).toEqual([]);
});
});
Integration Testing
Test error scenarios in integration tests:
describe('Service API Integration', () => {
it('should return 403 for insufficient permissions', async () => {
const apiKey = await createAPIKey(['read:users']); // No write permission
const response = await fetch(`${API_URL}/api/service/users`, {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: 'test@example.com' })
});
expect(response.status).toBe(403);
const error = await response.json();
expect(error.error_code).toBe('FORBIDDEN');
});
});
Debugging Tips
1. Use Timestamps for Correlation
Error timestamps help correlate client-side errors with server logs:
const error = await response.json();
console.error(`Error at ${error.timestamp}:`, error.error);
// Search server logs around this timestamp
2. Check Rate Limit Headers
Monitor rate limit headers to prevent hitting limits:
const response = await fetch(url, options);
console.log('Rate Limit:', {
limit: response.headers.get('X-RateLimit-Limit'),
remaining: response.headers.get('X-RateLimit-Remaining'),
reset: new Date(parseInt(response.headers.get('X-RateLimit-Reset')) * 1000)
});
3. Enable Request Logging
Log all API requests for debugging:
async function apiRequest(url, options) {
const startTime = Date.now();
console.log('API Request:', { url, method: options.method });
try {
const response = await fetch(url, options);
const duration = Date.now() - startTime;
console.log('API Response:', {
url,
status: response.status,
duration: `${duration}ms`
});
return response;
} catch (error) {
const duration = Date.now() - startTime;
console.error('API Request Failed:', {
url,
duration: `${duration}ms`,
error: error.message
});
throw error;
}
}
4. Use Request IDs
Generate unique request IDs for tracing:
function generateRequestId() {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
async function apiRequest(url, options) {
const requestId = generateRequestId();
options.headers['X-Request-ID'] = requestId;
console.log(`[${requestId}] Request:`, url);
try {
const response = await fetch(url, options);
console.log(`[${requestId}] Response:`, response.status);
return response;
} catch (error) {
console.error(`[${requestId}] Error:`, error.message);
throw error;
}
}
Error Recovery Patterns
Automatic Retry with Circuit Breaker
Implement circuit breaker pattern to prevent cascading failures:
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureCount = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
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;
}
}
}
const breaker = new CircuitBreaker();
async function makeRequest(url, options) {
return breaker.execute(() => fetch(url, options));
}
Graceful Degradation
Provide fallback behavior when API is unavailable:
async function getUserData(userId) {
try {
return await apiRequest(`/api/service/users/${userId}`);
} catch (error) {
if (error.status >= 500) {
// Server error - use cached data if available
const cached = localStorage.getItem(`user_${userId}`);
if (cached) {
console.warn('Using cached user data due to server error');
return JSON.parse(cached);
}
}
throw error;
}
}
Summary
Quick Reference
Always:
- Check HTTP status code first
- Parse error response JSON for details
- Log errors with context
- Show user-friendly messages
Never:
- Ignore error responses
- Expose raw errors to users
- Retry indefinitely
- Commit API keys to version control
Handle Specifically:
TOKEN_EXPIRED: Refresh and retryRATE_LIMIT_EXCEEDED: Backoff and retryORGANIZATION_NOT_ACTIVE: Redirect to status pageSERVICE_LIMIT_EXCEEDED: Show upgrade prompt
Additional Resources
- Authentication API Reference - Auth-specific error codes
- Platform Owner API Reference - Platform admin errors
- Service API Reference - Service API error scenarios
- Authentication Concepts - Authentication flows and rate limiting