Error Handling

MERU Documentation - Integration

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

CodeMeaningWhen It Occurs
200OKRequest succeeded
201CreatedResource created successfully
400Bad RequestInvalid request parameters
401UnauthorizedMissing or invalid API token
403ForbiddenToken lacks required permissions
404Not FoundResource doesn’t exist
409ConflictResource already exists or conflict
422Unprocessable EntityValid JSON but invalid data
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-side error
502Bad GatewayTemporary server issue
503Service UnavailableService 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?

Last updated: September 5, 2025