Error Handling
Handle API errors, webhook failures, and edge cases gracefully with proper error codes, retry strategies, and debugging techniques.
Prerequisites
- Understanding of HTTP status codes
- API integration experience
Error Handling
Robust error handling is crucial for reliable inbound email processing. This guide covers API error responses, webhook error handling, and best practices for building resilient integrations.
API Error Responses
The MERU API uses standard HTTP status codes and returns structured error responses to help you diagnose and handle issues effectively.
HTTP Status Codes
Code | Meaning | When It Occurs |
---|---|---|
200 | OK | Request succeeded |
201 | Created | Resource created successfully |
400 | Bad Request | Invalid request parameters |
401 | Unauthorized | Missing or invalid API token |
403 | Forbidden | Token lacks required permissions |
404 | Not Found | Resource doesnβt exist |
409 | Conflict | Resource already exists or conflict |
422 | Unprocessable Entity | Valid JSON but invalid data |
429 | Too Many Requests | Rate limit exceeded |
500 | Internal Server Error | Server-side error |
502 | Bad Gateway | Temporary server issue |
503 | Service Unavailable | Service temporarily down |
Error Response Format
All error responses follow this structure:
{
"error": "error_code",
"message": "Human-readable error description",
"details": {
"field": "specific_field",
"code": "validation_code",
"value": "invalid_value"
},
"request_id": "req_abc123def456",
"timestamp": "2025-09-05T10:30:00Z"
}
Common API Errors
Authentication Errors
401 Unauthorized - Missing Token
{
"error": "unauthorized",
"message": "Missing Authorization header",
"request_id": "req_abc123"
}
401 Unauthorized - Invalid Token
{
"error": "unauthorized",
"message": "Invalid API token",
"request_id": "req_abc123"
}
403 Forbidden - Insufficient Permissions
{
"error": "forbidden",
"message": "Token does not have required permissions for this operation",
"details": {
"required_permission": "addresses:write",
"token_permissions": ["addresses:read", "usage:read"]
},
"request_id": "req_abc123"
}
Validation Errors
400 Bad Request - Invalid Parameters
{
"error": "validation_failed",
"message": "The request contains invalid parameters",
"details": {
"webhook_url": ["Must be a valid HTTPS URL"],
"ttl_hours": ["Must be between 1 and 8760"]
},
"request_id": "req_abc123"
}
422 Unprocessable Entity - Business Logic Error
{
"error": "webhook_unreachable",
"message": "Webhook URL is not accessible",
"details": {
"url": "https://invalid-domain.example.com/webhook",
"error": "Connection timeout after 10 seconds"
},
"request_id": "req_abc123"
}
Resource Errors
404 Not Found
{
"error": "not_found",
"message": "Address not found",
"details": {
"address_id": "addr_nonexistent",
"resource": "address"
},
"request_id": "req_abc123"
}
409 Conflict
{
"error": "conflict",
"message": "Address limit exceeded",
"details": {
"limit": 100,
"current": 100,
"plan": "pro"
},
"request_id": "req_abc123"
}
Rate Limiting
429 Too Many Requests
{
"error": "rate_limit_exceeded",
"message": "Too many requests. Try again in 60 seconds.",
"details": {
"limit": 1000,
"window": "1 hour",
"retry_after": 60
},
"request_id": "req_abc123"
}
API Error Handling Best Practices
Implement Retry Logic
Handle transient errors with exponential backoff:
async function makeAPIRequest(url, options, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// Handle successful responses
if (response.ok) {
return await response.json();
}
// Handle different error types
if (response.status === 429) {
// Rate limited - respect retry-after header
const retryAfter = response.headers.get('retry-after') || 60;
await sleep(retryAfter * 1000);
continue;
}
if (response.status >= 500 && attempt < maxRetries) {
// Server error - retry with exponential backoff
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
await sleep(delay);
continue;
}
// Client errors (4xx) - don't retry
const error = await response.json();
throw new APIError(error, response.status);
} catch (err) {
if (attempt === maxRetries) throw err;
// Network error - retry with backoff
const delay = Math.pow(2, attempt) * 1000;
await sleep(delay);
}
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
Custom Error Classes
Create specific error types for better handling:
class APIError extends Error {
constructor(errorData, statusCode) {
super(errorData.message);
this.name = 'APIError';
this.code = errorData.error;
this.statusCode = statusCode;
this.details = errorData.details;
this.requestId = errorData.request_id;
}
}
class RateLimitError extends APIError {
constructor(errorData, statusCode) {
super(errorData, statusCode);
this.name = 'RateLimitError';
this.retryAfter = errorData.details?.retry_after;
}
}
// Usage
try {
const address = await createAddress(webhookUrl);
} catch (err) {
if (err instanceof RateLimitError) {
console.log(`Rate limited. Retry in ${err.retryAfter} seconds`);
// Implement backoff
} else if (err.code === 'webhook_unreachable') {
console.log('Webhook URL is not accessible:', err.details.url);
// Fix webhook URL
} else {
console.error('API error:', err.message);
// Generic error handling
}
}
Validation Before API Calls
Validate data client-side to reduce API errors:
function validateWebhookUrl(url) {
try {
const parsed = new URL(url);
if (parsed.protocol !== 'https:') {
throw new Error('Webhook URL must use HTTPS');
}
return true;
} catch (err) {
throw new Error('Invalid webhook URL format');
}
}
function validateTTL(hours) {
if (!Number.isInteger(hours) || hours < 1 || hours > 8760) {
throw new Error('TTL must be between 1 and 8760 hours');
}
return true;
}
// Validate before making API call
try {
validateWebhookUrl(webhookUrl);
validateTTL(ttlHours);
const address = await createAddress({
webhook_url: webhookUrl,
ttl_hours: ttlHours
});
} catch (validationError) {
console.error('Validation failed:', validationError.message);
}
Webhook Error Handling
Webhooks can fail for various reasons. Handle them gracefully to ensure reliable email processing.
Common Webhook Failures
Your Server Errors
Connection Timeout
- MERU waits 30 seconds for response
- Return 200 OK quickly, process asynchronously if needed
Server Errors (5xx)
- MERU will retry with exponential backoff
- Fix underlying issue causing server errors
Invalid Response Codes
- Always return 200 OK for successful processing
- Use 4xx codes for permanent failures only
Network Issues
DNS Resolution Failures
- Ensure your domain resolves correctly
- Use monitoring to detect DNS issues
SSL Certificate Problems
- Keep certificates valid and renewed
- Use monitoring to check certificate expiry
Webhook Error Recovery
Graceful Degradation
app.post('/webhook', async (req, res) => {
try {
// Verify signature first
if (!verifySignature(req)) {
return res.status(401).send('Invalid signature');
}
const email = req.body;
// Try primary processing
try {
await processPrimary(email);
} catch (primaryError) {
console.warn('Primary processing failed:', primaryError);
// Fall back to basic processing
await processBasic(email);
}
res.status(200).send('OK');
} catch (error) {
console.error('Webhook processing failed:', error);
// Store for manual processing
await storeFailedWebhook(req.body, error);
// Return 500 to trigger retry
res.status(500).send('Processing failed');
}
});
async function processPrimary(email) {
// Full processing with all features
await saveToDatabase(email);
await extractAttachments(email);
await sendNotifications(email);
await runAnalytics(email);
}
async function processBasic(email) {
// Essential processing only
await saveToDatabase(email);
}
Dead Letter Queue
Store failed webhooks for manual processing:
const Redis = require('redis');
const redis = Redis.createClient();
async function storeFailedWebhook(payload, error) {
const failedItem = {
payload,
error: error.message,
timestamp: new Date().toISOString(),
retry_count: payload.metadata?.retry_count || 0
};
await redis.lpush('failed_webhooks', JSON.stringify(failedItem));
}
// Process failed webhooks later
async function reprocessFailedWebhooks() {
while (true) {
const item = await redis.brpop('failed_webhooks', 10);
if (item) {
const failedWebhook = JSON.parse(item[1]);
try {
await processPrimary(failedWebhook.payload);
console.log('Reprocessed failed webhook successfully');
} catch (error) {
console.error('Reprocessing failed:', error);
// Put back in queue if retry count is low
if (failedWebhook.retry_count < 3) {
failedWebhook.retry_count++;
await redis.lpush('failed_webhooks', JSON.stringify(failedWebhook));
}
}
}
}
}
Idempotency
Handle duplicate webhook deliveries:
const processedEvents = new Map();
app.post('/webhook', async (req, res) => {
const email = req.body;
const eventId = email.event_id;
// Check if already processed
if (processedEvents.has(eventId)) {
console.log(`Event ${eventId} already processed`);
return res.status(200).send('OK');
}
try {
await processEmail(email);
// Mark as processed with TTL
processedEvents.set(eventId, Date.now());
// Clean up old entries (optional)
cleanupProcessedEvents();
res.status(200).send('OK');
} catch (error) {
console.error(`Processing failed for ${eventId}:`, error);
res.status(500).send('Processing failed');
}
});
function cleanupProcessedEvents() {
const oneHourAgo = Date.now() - (60 * 60 * 1000);
for (const [eventId, timestamp] of processedEvents.entries()) {
if (timestamp < oneHourAgo) {
processedEvents.delete(eventId);
}
}
}
Monitoring and Alerting
Health Checks
Implement webhook health checks:
app.get('/webhook/health', (req, res) => {
const health = {
status: 'healthy',
timestamp: new Date().toISOString(),
checks: {
database: checkDatabase(),
external_api: checkExternalAPI(),
disk_space: checkDiskSpace()
}
};
const allHealthy = Object.values(health.checks).every(check => check.status === 'ok');
res.status(allHealthy ? 200 : 503).json(health);
});
Error Metrics
Track important error metrics:
const metrics = {
webhook_requests_total: 0,
webhook_errors_total: 0,
processing_duration_ms: [],
signature_failures: 0
};
app.post('/webhook', async (req, res) => {
const startTime = Date.now();
metrics.webhook_requests_total++;
try {
if (!verifySignature(req)) {
metrics.signature_failures++;
return res.status(401).send('Invalid signature');
}
await processEmail(req.body);
const duration = Date.now() - startTime;
metrics.processing_duration_ms.push(duration);
res.status(200).send('OK');
} catch (error) {
metrics.webhook_errors_total++;
console.error('Webhook error:', error);
res.status(500).send('Processing failed');
}
});
// Expose metrics for monitoring
app.get('/metrics', (req, res) => {
const avgDuration = metrics.processing_duration_ms.reduce((a, b) => a + b, 0) /
metrics.processing_duration_ms.length || 0;
res.json({
...metrics,
error_rate: metrics.webhook_errors_total / metrics.webhook_requests_total,
avg_processing_duration_ms: avgDuration
});
});
Alerting
Set up alerts for critical issues:
function checkErrorRate() {
const errorRate = metrics.webhook_errors_total / metrics.webhook_requests_total;
if (errorRate > 0.05) { // 5% error rate
sendAlert({
severity: 'high',
message: `Webhook error rate is ${(errorRate * 100).toFixed(2)}%`,
metrics: {
total_requests: metrics.webhook_requests_total,
total_errors: metrics.webhook_errors_total
}
});
}
}
function sendAlert(alert) {
// Send to Slack, PagerDuty, email, etc.
console.error('ALERT:', alert);
}
// Check error rate every minute
setInterval(checkErrorRate, 60000);
Debugging Tips
Enable Debug Logging
const DEBUG = process.env.NODE_ENV === 'development';
function debugLog(message, data) {
if (DEBUG) {
console.log(`[DEBUG] ${message}:`, JSON.stringify(data, null, 2));
}
}
app.post('/webhook', (req, res) => {
debugLog('Webhook headers', req.headers);
debugLog('Webhook payload', req.body);
// ... rest of processing
});
Test with Curl
Test your webhook endpoint manually:
# Test webhook endpoint
curl -X POST https://yourapp.com/webhook \
-H "Content-Type: application/json" \
-H "Meru-Signature: v1,t=1693123456,s=abc123..." \
-d '{"event_id":"test","headers":{"subject":"Test"}}'
Use Request IDs
Include request IDs in logs for tracing:
const { v4: uuidv4 } = require('uuid');
app.use((req, res, next) => {
req.id = uuidv4();
next();
});
app.post('/webhook', (req, res) => {
console.log(`[${req.id}] Processing webhook`);
try {
// ... processing
console.log(`[${req.id}] Webhook processed successfully`);
} catch (error) {
console.error(`[${req.id}] Webhook processing failed:`, error);
}
});
Need Help?
- π§ Email: support@meruhook.com
- π¬ Chat: Available in your dashboard
- π More guides: API Reference, Webhooks
Related Documentation
Last updated: September 5, 2025