Aller au contenu principal

Error Handling & Best Practices

This guide covers error handling, rate limits, and best practices for integrating with the ISAO API.

📋 Table of Contents

❌ Error Response Format

All API errors follow a consistent JSON format:

{
"success": false,
"error": "Short error description",
"message": "Detailed human-readable error message",
"code": "ERROR_CODE", // Optional
"details": {}, // Optional, additional error context
"timestamp": "2024-11-08T10:30:00.000Z" // Optional
}

Example Error Response

{
"success": false,
"error": "Permission denied: contacts:write required",
"message": "This API key does not have the required permission: contacts:write",
"requiredPermission": "contacts:write",
"availablePermissions": ["contacts:read", "webhooks:write"]
}

🔢 HTTP Status Codes

Status CodeDescriptionCommon Causes
200SuccessRequest completed successfully
201CreatedResource created successfully
400Bad RequestInvalid request syntax or data
401UnauthorizedMissing or invalid API key
403ForbiddenInsufficient permissions
404Not FoundResource doesn't exist
409ConflictResource already exists
422Unprocessable EntityValidation errors
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-side error
503Service UnavailableTemporary service outage

🚨 Error Types

Authentication Errors (401)

Missing API Key

{
"success": false,
"error": "API key required in Authorization header",
"message": "Please provide a valid API key in the format: Authorization: Bearer YOUR_API_KEY"
}

Solution: Add the Authorization header with your API key.

curl -H "Authorization: Bearer YOUR_API_KEY" \
https://staging.isao.io/api/external/test

Invalid API Key Format

{
"success": false,
"error": "Invalid API key format",
"message": "API key appears to be invalid or too short"
}

Solution: Verify your API key is correctly formatted and not truncated.

Expired/Invalid API Key

{
"success": false,
"error": "Invalid API key",
"message": "The provided API key is invalid, expired, or inactive"
}

Solution: Check your API key status in the dashboard and regenerate if necessary.

Permission Errors (403)

Insufficient Permissions

{
"success": false,
"error": "Permission denied: contacts:write required",
"message": "This API key does not have the required permission: contacts:write",
"requiredPermission": "contacts:write",
"availablePermissions": ["contacts:read"]
}

Solution: Update your API key permissions in the dashboard.

Validation Errors (422)

Field Validation

{
"success": false,
"error": "Validation failed",
"details": {
"name": "Name is required and must be between 1-255 characters",
"phoneNumber": "Phone number is required",
"email": "Invalid email format"
}
}

Solution: Fix the invalid fields according to the details provided.

Required Fields Missing

{
"success": false,
"error": "Missing required fields",
"details": {
"missingFields": ["name", "phoneNumber"]
}
}

Resource Errors (404, 409)

Resource Not Found

{
"success": false,
"error": "Contact not found"
}

Solution: Verify the resource ID exists and belongs to your company.

Duplicate Resource

{
"success": false,
"error": "Contact with this phone number already exists",
"existingContactId": "contact_123"
}

Solution: Use the existing resource ID or update instead of creating.

Rate Limiting Errors (429)

{
"success": false,
"error": "Rate limit exceeded",
"message": "Too many requests. Please slow down.",
"retryAfter": 60,
"limits": {
"hourly": 1000,
"daily": 10000
}
}

Solution: Implement exponential backoff and respect the retryAfter value.

⚡ Rate Limiting

Default Limits

Limit TypeDefault ValueDescription
Per Hour1000 requestsResets every hour
Per Day10000 requestsResets at midnight UTC
Concurrent10 requestsMaximum simultaneous requests

Rate Limit Headers

X-RateLimit-Hourly-Limit: 1000
X-RateLimit-Hourly-Remaining: 999
X-RateLimit-Hourly-Reset: 1699459200
X-RateLimit-Daily-Limit: 10000
X-RateLimit-Daily-Remaining: 9999
X-RateLimit-Daily-Reset: 1699459200

Handling Rate Limits

async function makeApiRequest(url, options) {
try {
const response = await fetch(url, options);

// Check rate limit headers
const hourlyRemaining = response.headers.get('X-RateLimit-Hourly-Remaining');
if (hourlyRemaining && parseInt(hourlyRemaining) < 10) {
console.warn('Approaching hourly rate limit');
}

if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
console.log(`Rate limited, retrying after ${retryAfter} seconds`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
return makeApiRequest(url, options); // Retry
}

return response.json();
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}

✅ Best Practices

1. Implement Proper Error Handling

class IsaoAPIClient {
async makeRequest(endpoint, options = {}) {
try {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
...options.headers
},
...options
});

const data = await response.json();

if (!response.ok) {
throw new IsaoAPIError(data, response.status);
}

return data;
} catch (error) {
if (error instanceof IsaoAPIError) {
throw error;
}
throw new IsaoAPIError({ error: 'Network error', message: error.message }, 0);
}
}
}

class IsaoAPIError extends Error {
constructor(errorData, statusCode) {
super(errorData.message || errorData.error);
this.name = 'IsaoAPIError';
this.statusCode = statusCode;
this.errorData = errorData;
}

isAuthError() {
return this.statusCode === 401;
}

isPermissionError() {
return this.statusCode === 403;
}

isRateLimitError() {
return this.statusCode === 429;
}

isValidationError() {
return this.statusCode === 422;
}
}

2. Use Pagination for Large Datasets

async function getAllContacts(apiClient) {
const allContacts = [];
let page = 1;
let hasMore = true;

while (hasMore) {
try {
const response = await apiClient.makeRequest('/contacts', {
method: 'GET',
params: { page, limit: 200 } // Use maximum page size
});

allContacts.push(...response.data);

hasMore = response.meta.page < response.meta.totalPages;
page++;

// Small delay to avoid rate limits
if (hasMore) {
await new Promise(resolve => setTimeout(resolve, 100));
}
} catch (error) {
if (error.isRateLimitError()) {
// Wait and retry
await new Promise(resolve => setTimeout(resolve, 60000));
continue;
}
throw error;
}
}

return allContacts;
}

3. Validate Data Before Sending

function validateContact(contact) {
const errors = {};

if (!contact.name || contact.name.length < 1 || contact.name.length > 255) {
errors.name = 'Name is required and must be between 1-255 characters';
}

if (!contact.phoneNumber) {
errors.phoneNumber = 'Phone number is required';
}

if (contact.email && !isValidEmail(contact.email)) {
errors.email = 'Invalid email format';
}

if (Object.keys(errors).length > 0) {
throw new ValidationError('Contact validation failed', errors);
}

return true;
}

function isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

4. Implement Idempotency

class IdempotentClient {
constructor(apiClient) {
this.apiClient = apiClient;
this.requestCache = new Map();
}

async createContact(contactData, idempotencyKey = null) {
// Generate idempotency key if not provided
if (!idempotencyKey) {
idempotencyKey = this.generateIdempotencyKey(contactData);
}

// Check if we've already made this request
if (this.requestCache.has(idempotencyKey)) {
return this.requestCache.get(idempotencyKey);
}

try {
const result = await this.apiClient.makeRequest('/contacts', {
method: 'POST',
body: JSON.stringify(contactData),
headers: {
'Idempotency-Key': idempotencyKey
}
});

// Cache successful result
this.requestCache.set(idempotencyKey, result);

// Clean up cache after 1 hour
setTimeout(() => {
this.requestCache.delete(idempotencyKey);
}, 3600000);

return result;
} catch (error) {
if (error.statusCode === 409) {
// Conflict - resource might already exist
console.log('Contact already exists, fetching existing contact');
return this.getContactByPhone(contactData.phoneNumber);
}
throw error;
}
}

generateIdempotencyKey(data) {
const crypto = require('crypto');
return crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex');
}
}

🔄 Retry Strategies

Exponential Backoff

class RetryableClient {
async makeRequestWithRetry(endpoint, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await this.makeRequest(endpoint, options);
} catch (error) {
if (this.shouldRetry(error, attempt, maxRetries)) {
const delay = this.calculateBackoff(attempt);
console.log(`Request failed, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries + 1})`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
}

shouldRetry(error, attempt, maxRetries) {
// Don't retry if we've exceeded max attempts
if (attempt >= maxRetries) {
return false;
}

// Retry on rate limits and server errors
return error.isRateLimitError() || error.statusCode >= 500;
}

calculateBackoff(attempt) {
// Exponential backoff: 1s, 2s, 4s, 8s...
const baseDelay = 1000;
const maxDelay = 30000; // Cap at 30 seconds
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);

// Add jitter to avoid thundering herd
return delay + Math.random() * 1000;
}
}

Circuit Breaker Pattern

class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.threshold = threshold;
this.timeout = timeout;
this.failureCount = 0;
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;
}
}
}

📊 Monitoring & Debugging

Request Logging

class LoggingClient {
constructor(apiKey, options = {}) {
this.apiKey = apiKey;
this.logger = options.logger || console;
this.baseUrl = 'https://staging.isao.io/api/external';
}

async makeRequest(endpoint, options = {}) {
const requestId = this.generateRequestId();
const startTime = Date.now();

this.logger.info('API Request', {
requestId,
method: options.method || 'GET',
endpoint,
timestamp: new Date().toISOString()
});

try {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'X-Request-ID': requestId,
...options.headers
},
...options
});

const duration = Date.now() - startTime;
const data = await response.json();

this.logger.info('API Response', {
requestId,
statusCode: response.status,
duration,
success: response.ok
});

if (!response.ok) {
this.logger.error('API Error', {
requestId,
statusCode: response.status,
error: data.error,
message: data.message
});
throw new IsaoAPIError(data, response.status);
}

return data;
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error('API Request Failed', {
requestId,
duration,
error: error.message,
stack: error.stack
});
throw error;
}
}

generateRequestId() {
return Math.random().toString(36).substring(7);
}
}

Health Check Implementation

class HealthChecker {
constructor(apiClient) {
this.apiClient = apiClient;
this.isHealthy = true;
this.lastCheck = null;
this.checkInterval = 30000; // 30 seconds
}

async checkHealth() {
try {
const response = await this.apiClient.makeRequest('/test');
this.isHealthy = true;
this.lastCheck = Date.now();
return { healthy: true, timestamp: new Date().toISOString() };
} catch (error) {
this.isHealthy = false;
this.lastCheck = Date.now();
return {
healthy: false,
timestamp: new Date().toISOString(),
error: error.message
};
}
}

startHealthCheck() {
setInterval(() => {
this.checkHealth().then(result => {
if (result.healthy) {
console.log('✅ API health check passed');
} else {
console.error('❌ API health check failed:', result.error);
}
});
}, this.checkInterval);
}

getStatus() {
return {
healthy: this.isHealthy,
lastCheck: this.lastCheck ? new Date(this.lastCheck).toISOString() : null
};
}
}

Error Reporting

class ErrorReporter {
constructor(webhookUrl) {
this.webhookUrl = webhookUrl;
}

async reportError(error, context = {}) {
const errorReport = {
timestamp: new Date().toISOString(),
error: {
name: error.name,
message: error.message,
stack: error.stack,
statusCode: error.statusCode
},
context,
environment: process.env.NODE_ENV || 'development'
};

try {
await fetch(this.webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(errorReport)
});
} catch (reportingError) {
console.error('Failed to report error:', reportingError);
}
}
}

// Usage
const errorReporter = new ErrorReporter('https://your-error-tracking-service.com/webhook');

try {
await apiClient.createContact(contactData);
} catch (error) {
await errorReporter.reportError(error, {
operation: 'createContact',
contactData: { ...contactData, phoneNumber: '[REDACTED]' } // Don't log sensitive data
});
throw error;
}

🔧 Development Tools

API Response Validator

class ResponseValidator {
validateContact(contact) {
const requiredFields = ['id', 'name', 'phoneNumber', 'createdAt', 'updatedAt'];
const missingFields = requiredFields.filter(field => !contact[field]);

if (missingFields.length > 0) {
throw new Error(`Missing required fields: ${missingFields.join(', ')}`);
}

if (contact.email && !this.isValidEmail(contact.email)) {
throw new Error('Invalid email format in response');
}

return true;
}

validateDeal(deal) {
const requiredFields = ['id', 'title', 'value', 'status', 'createdAt', 'updatedAt'];
const missingFields = requiredFields.filter(field => deal[field] === undefined);

if (missingFields.length > 0) {
throw new Error(`Missing required fields: ${missingFields.join(', ')}`);
}

if (typeof deal.value !== 'number' || deal.value < 0) {
throw new Error('Deal value must be a non-negative number');
}

return true;
}

isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}

📈 Performance Tips

  1. Batch Operations: Group multiple operations when possible
  2. Use Pagination: Don't fetch all data at once
  3. Cache Responses: Cache frequently accessed data
  4. Parallel Requests: Make independent requests in parallel
  5. Connection Pooling: Reuse HTTP connections
  6. Compress Requests: Use gzip compression for large payloads
  7. Monitor Performance: Track response times and error rates