# Error Handling

Learn how to handle errors gracefully when using the Zaits API, including retry strategies, error classification, and debugging techniques.

## Error Response Format

All Zaits API errors follow a consistent format:

```json
{
  "success": false,
  "error": {
    "code": "error_code",
    "message": "Human-readable error message",
    "details": {
      "additional": "context-specific information"
    }
  }
}
```

## HTTP Status Codes

| Status Code | Meaning               | When It Occurs                              |
| ----------- | --------------------- | ------------------------------------------- |
| **200**     | Success               | Request completed successfully              |
| **400**     | Bad Request           | Invalid parameters or malformed request     |
| **401**     | Unauthorized          | Missing or invalid API key                  |
| **403**     | Forbidden             | Insufficient permissions or limits exceeded |
| **404**     | Not Found             | Endpoint or resource not found              |
| **413**     | Payload Too Large     | File size exceeds limits                    |
| **429**     | Too Many Requests     | Rate limit exceeded                         |
| **500**     | Internal Server Error | Temporary server issue                      |
| **502/503** | Service Unavailable   | Server maintenance or overload              |

***

## Error Categories

### 1. Authentication Errors (401)

#### Common Authentication Issues

| Error Code               | Description                | Solution                                 |
| ------------------------ | -------------------------- | ---------------------------------------- |
| `missing_api_key`        | No Authorization header    | Add `Authorization: Bearer YOUR_API_KEY` |
| `invalid_api_key`        | API key is invalid/expired | Check key in dashboard, generate new one |
| `api_key_format_invalid` | Malformed API key          | Ensure key starts with `sk_`             |

#### Example Response

```json
{
  "success": false,
  "error": {
    "code": "invalid_api_key",
    "message": "The provided API key is invalid or has expired",
    "details": {
      "key_prefix": "sk_1234..."
    }
  }
}
```

#### Handling Authentication Errors

```javascript
async function handleAuthError(error) {
  if (error.code === 'invalid_api_key') {
    // Log security event
    console.error('API key invalid - possible security issue');
    
    // Notify admin
    await notifyAdmin('Invalid API key detected', {
      timestamp: new Date(),
      endpoint: error.endpoint,
      ip: error.clientIp
    });
    
    // Don't retry - require manual intervention
    throw new Error('Authentication failed - check API key');
  }
}
```

### 2. Authorization Errors (403)

#### Common Authorization Issues

| Error Code                 | Description                        | Solution                        |
| -------------------------- | ---------------------------------- | ------------------------------- |
| `insufficient_permissions` | API key lacks required permissions | Enable permissions in dashboard |
| `ip_not_allowed`           | Request from non-whitelisted IP    | Add IP to allowlist             |
| `subscription_required`    | Feature requires paid plan         | Upgrade subscription            |
| `usage_limit_exceeded`     | Monthly usage limit reached        | Upgrade plan or wait for reset  |

#### Example Response

```json
{
  "success": false,
  "error": {
    "code": "insufficient_permissions",
    "message": "Your API key doesn't have write permissions",
    "details": {
      "required_permission": "write",
      "current_permissions": ["read"]
    }
  }
}
```

### 3. Validation Errors (400)

#### Image Validation Errors

| Error Code                | Description                      | Solution                                 |
| ------------------------- | -------------------------------- | ---------------------------------------- |
| `no_face_detected`        | No face found in image           | Use different image with clear face      |
| `multiple_faces_detected` | Multiple faces when one expected | Crop to single face or use detection API |
| `image_too_small`         | Image resolution too low         | Use higher resolution image              |
| `image_quality_low`       | Image quality insufficient       | Use better lighting/focus                |
| `unsupported_format`      | Invalid file format              | Use JPG, PNG, or WebP                    |

#### Example Response

```json
{
  "success": false,
  "error": {
    "code": "no_face_detected",
    "message": "No face was detected in the provided image",
    "details": {
      "image": "image1",
      "min_face_size": "80x80",
      "suggestions": [
        "Ensure the face is clearly visible",
        "Check image lighting and quality",
        "Try a different angle or photo"
      ]
    }
  }
}
```

#### Handling Validation Errors

```python
def handle_validation_error(error):
    """Handle validation errors with specific guidance"""
    
    error_handlers = {
        'no_face_detected': handle_no_face_error,
        'image_too_small': handle_small_image_error,
        'image_quality_low': handle_quality_error,
        'unsupported_format': handle_format_error
    }
    
    handler = error_handlers.get(error['code'])
    if handler:
        return handler(error)
    
    # Generic validation error
    return {
        'retry': False,
        'user_message': error['message'],
        'suggestions': error.get('details', {}).get('suggestions', [])
    }

def handle_no_face_error(error):
    return {
        'retry': False,
        'user_message': 'Please upload an image with a clearly visible face',
        'suggestions': [
            'Make sure the face takes up at least 1/4 of the image',
            'Ensure good lighting',
            'Face should be looking towards the camera'
        ]
    }
```

### 4. Insufficient Credits (429)

#### Insufficient Credits Error Response

```json
{
  "success": false,
  "error": "insufficient_credits",
  "message": "Insufficient credits. This request costs 2 credit(s). Your current balance is 0 credits.",
  "error_code": "INSUFFICIENT_CREDITS",
  "details": {
    "credit_cost": 2,
    "credit_balance": 0,
    "needs_credits": true,
    "upgrade_message": "Upgrade to Professional for 20,000 credits per month",
    "reset_date": "2026-03-01T00:00:00.000Z"
  },
  "retry_after": 518400
}
```

#### Exponential Backoff Implementation

```javascript
class ExponentialBackoff {
  constructor(baseDelay = 1000, maxDelay = 30000, maxRetries = 5) {
    this.baseDelay = baseDelay;
    this.maxDelay = maxDelay;
    this.maxRetries = maxRetries;
  }
  
  async execute(fn, context = {}) {
    for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
      try {
        return await fn();
      } catch (error) {
        // Only retry on rate limit or server errors
        if (!this.shouldRetry(error, attempt)) {
          throw error;
        }
        
        const delay = this.calculateDelay(attempt, error);
        console.log(`Rate limited. Retrying in ${delay}ms (attempt ${attempt + 1}/${this.maxRetries + 1})`);
        
        await this.sleep(delay);
      }
    }
    
    throw new Error('Maximum retries exceeded');
  }
  
  shouldRetry(error, attempt) {
    if (attempt >= this.maxRetries) return false;
    
    // Retry on rate limits and server errors
    return error.status === 429 || error.status >= 500;
  }
  
  calculateDelay(attempt, error) {
    // Use server's retry-after header if available
    if (error.details?.retry_after) {
      return error.details.retry_after * 1000;
    }
    
    // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 30s (max)
    const delay = Math.min(
      this.baseDelay * Math.pow(2, attempt),
      this.maxDelay
    );
    
    // Add jitter to prevent thundering herd
    const jitter = Math.random() * 0.3; // ±30%
    return Math.floor(delay * (1 + jitter));
  }
  
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Usage
const backoff = new ExponentialBackoff();

async function makeApiCallWithBackoff(endpoint, data) {
  return backoff.execute(async () => {
    const response = await fetch(endpoint, {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer YOUR_API_KEY',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    });
    
    if (!response.ok) {
      const error = await response.json();
      const apiError = new Error(error.error.message);
      apiError.status = response.status;
      apiError.code = error.error.code;
      apiError.details = error.error.details;
      throw apiError;
    }
    
    return response.json();
  });
}
```

### 5. Server Errors (5xx)

#### Server Error Handling Strategy

```python
import logging
import time
from datetime import datetime, timedelta

class ServerErrorHandler:
    def __init__(self, max_retries=3, base_delay=2):
        self.max_retries = max_retries
        self.base_delay = base_delay
        self.logger = logging.getLogger(__name__)
    
    def handle_server_error(self, error, attempt=0):
        """Handle server errors with appropriate retry logic"""
        
        if attempt >= self.max_retries:
            self.logger.error(f"Server error after {attempt} retries: {error}")
            raise ServerError(f"Server unavailable after {attempt} retries")
        
        # Calculate delay with exponential backoff
        delay = self.base_delay * (2 ** attempt)
        
        # Add jitter
        import random
        jitter = random.uniform(0.5, 1.5)
        delay = delay * jitter
        
        self.logger.warning(f"Server error (attempt {attempt + 1}). Retrying in {delay:.2f}s")
        time.sleep(delay)
        
        return True  # Indicate retry should happen
    
    def is_retriable_error(self, status_code):
        """Determine if error is worth retrying"""
        retriable_codes = [500, 502, 503, 504]
        return status_code in retriable_codes

# Usage in API client
class ZaitsAPIClient:
    def __init__(self, api_key):
        self.api_key = api_key
        self.error_handler = ServerErrorHandler()
    
    def make_request(self, endpoint, data):
        attempt = 0
        
        while attempt <= self.error_handler.max_retries:
            try:
                response = requests.post(
                    f"https://api.zaits.net{endpoint}",
                    headers={'Authorization': f'Bearer {self.api_key}'},
                    json=data,
                    timeout=30
                )
                
                if response.status_code == 200:
                    return response.json()
                
                elif self.error_handler.is_retriable_error(response.status_code):
                    if self.error_handler.handle_server_error(response.text, attempt):
                        attempt += 1
                        continue
                
                else:
                    # Non-retriable error
                    response.raise_for_status()
                    
            except requests.exceptions.RequestException as e:
                if attempt >= self.error_handler.max_retries:
                    raise NetworkError(f"Network error after retries: {e}")
                
                self.error_handler.handle_server_error(str(e), attempt)
                attempt += 1
```

***

## Error Recovery Patterns

### 1. Circuit Breaker Pattern

```javascript
class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.recoveryTimeout = options.recoveryTimeout || 60000;
    this.monitoringPeriod = options.monitoringPeriod || 10000;
    
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failureCount = 0;
    this.nextAttempt = Date.now();
    this.successCount = 0;
  }
  
  async call(operation) {
    if (this.state === 'OPEN') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('Circuit breaker is OPEN');
      }
      this.state = 'HALF_OPEN';
      this.successCount = 0;
    }
    
    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  onSuccess() {
    this.failureCount = 0;
    
    if (this.state === 'HALF_OPEN') {
      this.successCount++;
      if (this.successCount >= 3) { // Require 3 successes to fully close
        this.state = 'CLOSED';
      }
    }
  }
  
  onFailure() {
    this.failureCount++;
    
    if (this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.recoveryTimeout;
    }
  }
  
  getState() {
    return {
      state: this.state,
      failureCount: this.failureCount,
      nextAttempt: new Date(this.nextAttempt).toISOString()
    };
  }
}
```

### 2. Fallback Mechanisms

```python
class FallbackManager:
    def __init__(self):
        self.fallback_strategies = {}
    
    def register_fallback(self, operation, fallback_fn):
        """Register a fallback function for an operation"""
        self.fallback_strategies[operation] = fallback_fn
    
    async def execute_with_fallback(self, operation, primary_fn, *args, **kwargs):
        """Execute primary function with fallback on failure"""
        try:
            return await primary_fn(*args, **kwargs)
        except Exception as e:
            fallback = self.fallback_strategies.get(operation)
            if fallback:
                logging.warning(f"Primary operation failed, using fallback: {e}")
                return await fallback(*args, **kwargs)
            raise

# Usage example
fallback_manager = FallbackManager()

# Register fallbacks
fallback_manager.register_fallback(
    'face_verification', 
    fallback_local_verification
)
fallback_manager.register_fallback(
    'ocr_extraction',
    fallback_queue_for_later
)

async def fallback_local_verification(image1, image2):
    """Local fallback for face verification"""
    # Implement simple local comparison
    return {
        'verified': False,  # Conservative default
        'confidence': 0.0,
        'source': 'local_fallback',
        'message': 'Used local fallback due to API unavailability'
    }

async def fallback_queue_for_later(document):
    """Queue document for later processing"""
    await queue.add('ocr_processing', {
        'document': document,
        'timestamp': datetime.now().isoformat()
    })
    
    return {
        'text': '',
        'status': 'queued',
        'message': 'Document queued for processing when API is available'
    }
```

### 3. Dead Letter Queue

```javascript
class DeadLetterQueue {
  constructor(storage, retryLimit = 3) {
    this.storage = storage;
    this.retryLimit = retryLimit;
  }
  
  async processWithDLQ(operation, data, context = {}) {
    const dlqKey = `dlq:${context.id || Date.now()}`;
    
    try {
      return await operation(data);
    } catch (error) {
      const retryCount = context.retryCount || 0;
      
      if (retryCount >= this.retryLimit) {
        // Send to dead letter queue
        await this.storage.set(dlqKey, {
          data,
          error: error.message,
          retryCount,
          timestamp: new Date().toISOString(),
          context
        });
        
        console.error(`Operation failed permanently, sent to DLQ: ${dlqKey}`);
        return { status: 'failed', dlqId: dlqKey };
      }
      
      // Schedule retry
      setTimeout(() => {
        this.processWithDLQ(operation, data, {
          ...context,
          retryCount: retryCount + 1
        });
      }, Math.pow(2, retryCount) * 1000);
      
      throw error;
    }
  }
  
  async reprocessDLQ() {
    const dlqItems = await this.storage.getPattern('dlq:*');
    
    for (const item of dlqItems) {
      try {
        console.log(`Reprocessing DLQ item: ${item.id}`);
        // Attempt to reprocess
        // If successful, remove from DLQ
        await this.storage.delete(item.id);
      } catch (error) {
        console.error(`Failed to reprocess ${item.id}: ${error}`);
      }
    }
  }
}
```

***

## Error Monitoring and Alerting

### 1. Error Tracking

```javascript
class ErrorTracker {
  constructor(options = {}) {
    this.errors = new Map();
    this.alertThresholds = options.alertThresholds || {
      error_rate: 0.05,    // 5% error rate
      consecutive_errors: 10,
      time_window: 300000  // 5 minutes
    };
  }
  
  trackError(error, context = {}) {
    const errorKey = `${error.code}_${context.endpoint}`;
    const timestamp = Date.now();
    
    if (!this.errors.has(errorKey)) {
      this.errors.set(errorKey, []);
    }
    
    this.errors.get(errorKey).push({
      timestamp,
      error,
      context
    });
    
    this.checkAlertConditions(errorKey);
  }
  
  checkAlertConditions(errorKey) {
    const errors = this.errors.get(errorKey);
    const now = Date.now();
    
    // Remove old errors outside time window
    const recentErrors = errors.filter(
      e => (now - e.timestamp) < this.alertThresholds.time_window
    );
    
    // Check consecutive errors
    if (recentErrors.length >= this.alertThresholds.consecutive_errors) {
      this.sendAlert('consecutive_errors', {
        errorKey,
        count: recentErrors.length,
        timeWindow: this.alertThresholds.time_window / 1000
      });
    }
  }
  
  async sendAlert(type, data) {
    const alert = {
      type,
      timestamp: new Date().toISOString(),
      data,
      severity: this.getSeverity(type)
    };
    
    console.error('ALERT:', alert);
    
    // Send to monitoring service
    await this.notifyMonitoringService(alert);
  }
  
  getSeverity(alertType) {
    const severityMap = {
      consecutive_errors: 'high',
      error_rate_exceeded: 'medium',
      api_unavailable: 'critical'
    };
    
    return severityMap[alertType] || 'low';
  }
}
```

### 2. Health Check Implementation

```python
from datetime import datetime, timedelta
import asyncio

class HealthChecker:
    def __init__(self, api_client, check_interval=300):
        self.api_client = api_client
        self.check_interval = check_interval  # 5 minutes
        self.health_status = {
            'status': 'unknown',
            'last_check': None,
            'consecutive_failures': 0,
            'endpoints': {}
        }
    
    async def start_monitoring(self):
        """Start continuous health monitoring"""
        while True:
            try:
                await self.perform_health_check()
                await asyncio.sleep(self.check_interval)
            except Exception as e:
                logging.error(f"Health check error: {e}")
                await asyncio.sleep(60)  # Shorter retry on error
    
    async def perform_health_check(self):
        """Perform comprehensive health check"""
        endpoints = [
            ('usage', '/v1/usage/summary'),
            ('face', '/v1/face/verify'),
            ('ocr', '/v1/ocr/extract/id')
        ]
        
        endpoint_results = {}
        overall_healthy = True
        
        for name, endpoint in endpoints:
            try:
                start_time = datetime.now()
                result = await self.check_endpoint(endpoint)
                response_time = (datetime.now() - start_time).total_seconds()
                
                endpoint_results[name] = {
                    'status': 'healthy',
                    'response_time': response_time,
                    'last_check': datetime.now().isoformat()
                }
                
            except Exception as e:
                overall_healthy = False
                endpoint_results[name] = {
                    'status': 'unhealthy',
                    'error': str(e),
                    'last_check': datetime.now().isoformat()
                }
        
        # Update health status
        self.health_status.update({
            'status': 'healthy' if overall_healthy else 'degraded',
            'last_check': datetime.now().isoformat(),
            'consecutive_failures': 0 if overall_healthy else self.health_status['consecutive_failures'] + 1,
            'endpoints': endpoint_results
        })
        
        # Alert on consecutive failures
        if self.health_status['consecutive_failures'] >= 3:
            await self.send_health_alert()
    
    async def check_endpoint(self, endpoint):
        """Check specific endpoint health"""
        if endpoint == '/v1/usage/summary':
            return await self.api_client.get_usage_summary()
        else:
            # For endpoints requiring data, check if they respond to OPTIONS
            return await self.api_client.check_endpoint_availability(endpoint)
    
    async def send_health_alert(self):
        """Send health alert to monitoring system"""
        alert = {
            'type': 'api_health_degraded',
            'consecutive_failures': self.health_status['consecutive_failures'],
            'unhealthy_endpoints': [
                name for name, status in self.health_status['endpoints'].items()
                if status['status'] == 'unhealthy'
            ],
            'timestamp': datetime.now().isoformat()
        }
        
        # Send alert (implement your notification logic)
        logging.critical(f"API Health Alert: {alert}")
```

***

## Debugging Techniques

### 1. Request/Response Logging

```javascript
class APILogger {
  constructor(options = {}) {
    this.logLevel = options.logLevel || 'info';
    this.logRequests = options.logRequests !== false;
    this.logResponses = options.logResponses !== false;
    this.logErrors = options.logErrors !== false;
    this.maskSensitive = options.maskSensitive !== false;
  }
  
  async logRequest(endpoint, data, headers) {
    if (!this.logRequests) return;
    
    const logData = {
      timestamp: new Date().toISOString(),
      type: 'request',
      endpoint,
      method: 'POST',
      headers: this.maskSensitive ? this.maskHeaders(headers) : headers,
      data: this.maskSensitive ? this.maskRequestData(data) : data
    };
    
    console.log('API Request:', JSON.stringify(logData, null, 2));
  }
  
  async logResponse(endpoint, response, responseTime) {
    if (!this.logResponses) return;
    
    const logData = {
      timestamp: new Date().toISOString(),
      type: 'response',
      endpoint,
      success: response.success,
      responseTime,
      data: this.maskSensitive ? this.maskResponseData(response) : response
    };
    
    console.log('API Response:', JSON.stringify(logData, null, 2));
  }
  
  async logError(endpoint, error, context = {}) {
    if (!this.logErrors) return;
    
    const logData = {
      timestamp: new Date().toISOString(),
      type: 'error',
      endpoint,
      error: {
        code: error.code,
        message: error.message,
        status: error.status
      },
      context
    };
    
    console.error('API Error:', JSON.stringify(logData, null, 2));
  }
  
  maskHeaders(headers) {
    const masked = { ...headers };
    if (masked.Authorization) {
      masked.Authorization = 'Bearer sk_***';
    }
    return masked;
  }
  
  maskRequestData(data) {
    // Remove or mask sensitive data in requests
    const masked = { ...data };
    if (masked.image) {
      masked.image = '[IMAGE_DATA]';
    }
    return masked;
  }
}
```

### 2. Error Context Collection

```python
import traceback
import sys
from datetime import datetime

class ErrorContext:
    @staticmethod
    def collect_context(error, request_data=None):
        """Collect comprehensive error context"""
        return {
            'error': {
                'type': type(error).__name__,
                'message': str(error),
                'traceback': traceback.format_exc()
            },
            'request': {
                'data': ErrorContext.sanitize_request_data(request_data),
                'timestamp': datetime.now().isoformat()
            },
            'system': {
                'python_version': sys.version,
                'platform': sys.platform
            },
            'environment': ErrorContext.get_environment_info()
        }
    
    @staticmethod
    def sanitize_request_data(data):
        """Remove sensitive data from request context"""
        if not data:
            return None
        
        sanitized = data.copy()
        
        # Remove binary data
        for key in ['image', 'image1', 'image2', 'file', 'document']:
            if key in sanitized:
                sanitized[key] = f'[{key.upper()}_DATA_REMOVED]'
        
        return sanitized
    
    @staticmethod
    def get_environment_info():
        """Collect relevant environment information"""
        import os
        return {
            'api_environment': os.getenv('ENVIRONMENT', 'unknown'),
            'api_version': os.getenv('API_VERSION', 'unknown'),
            'service_name': os.getenv('SERVICE_NAME', 'unknown')
        }
```

***

## Testing Error Scenarios

### 1. Error Simulation for Testing

```javascript
// test-utils/api-mocker.js
class APIErrorSimulator {
  constructor() {
    this.errorScenarios = new Map();
  }
  
  addScenario(endpoint, errorType, config = {}) {
    if (!this.errorScenarios.has(endpoint)) {
      this.errorScenarios.set(endpoint, []);
    }
    
    this.errorScenarios.get(endpoint).push({
      type: errorType,
      probability: config.probability || 1.0,
      delay: config.delay || 0,
      ...config
    });
  }
  
  async simulateCall(endpoint, originalCall) {
    const scenarios = this.errorScenarios.get(endpoint) || [];
    
    for (const scenario of scenarios) {
      if (Math.random() < scenario.probability) {
        await this.sleep(scenario.delay);
        throw this.generateError(scenario);
      }
    }
    
    return originalCall();
  }
  
  generateError(scenario) {
    const errors = {
      rate_limit: {
        status: 429,
        code: 'rate_limit_exceeded',
        message: 'Too many requests',
        details: { retry_after: 60 }
      },
      server_error: {
        status: 500,
        code: 'internal_server_error',
        message: 'Internal server error'
      },
      validation_error: {
        status: 400,
        code: 'no_face_detected',
        message: 'No face detected in image'
      },
      auth_error: {
        status: 401,
        code: 'invalid_api_key',
        message: 'Invalid API key'
      }
    };
    
    const errorTemplate = errors[scenario.type];
    const error = new Error(errorTemplate.message);
    Object.assign(error, errorTemplate, scenario.override || {});
    
    return error;
  }
  
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Usage in tests
describe('Error Handling', () => {
  let apiSimulator;
  
  beforeEach(() => {
    apiSimulator = new APIErrorSimulator();
  });
  
  it('should handle rate limit errors with exponential backoff', async () => {
    apiSimulator.addScenario('/v1/face/verify', 'rate_limit', {
      probability: 1.0,
      delay: 100
    });
    
    const startTime = Date.now();
    
    try {
      await apiSimulator.simulateCall('/v1/face/verify', mockApiCall);
      fail('Expected rate limit error');
    } catch (error) {
      expect(error.status).toBe(429);
      expect(Date.now() - startTime).toBeGreaterThan(100);
    }
  });
});
```

### 2. Error Recovery Testing

```python
import pytest
import asyncio
from unittest.mock import AsyncMock, MagicMock

class TestErrorRecovery:
    @pytest.fixture
    def api_client(self):
        return MagicMock()
    
    @pytest.fixture  
    def circuit_breaker(self):
        return CircuitBreaker(
            failure_threshold=3,
            recovery_timeout=1000,
            monitoring_period=500
        )
    
    @pytest.mark.asyncio
    async def test_circuit_breaker_opens_after_failures(self, circuit_breaker, api_client):
        """Test circuit breaker opens after consecutive failures"""
        
        # Mock API to always fail
        api_client.face_verify = AsyncMock(side_effect=Exception("API Error"))
        
        # Make requests until circuit opens
        for i in range(5):
            try:
                await circuit_breaker.call(lambda: api_client.face_verify())
            except Exception:
                pass
        
        # Circuit should now be open
        assert circuit_breaker.state == 'OPEN'
        
        # Next call should fail immediately without calling API
        with pytest.raises(Exception, match="Circuit breaker is OPEN"):
            await circuit_breaker.call(lambda: api_client.face_verify())
        
        # API should not be called when circuit is open
        assert api_client.face_verify.call_count == 3  # Only during threshold detection
    
    @pytest.mark.asyncio
    async def test_fallback_on_primary_failure(self, api_client):
        """Test fallback mechanism when primary service fails"""
        
        fallback_manager = FallbackManager()
        
        # Mock primary to fail
        primary_fn = AsyncMock(side_effect=Exception("Primary service down"))
        
        # Mock fallback to succeed
        fallback_fn = AsyncMock(return_value={'status': 'fallback_used'})
        fallback_manager.register_fallback('test_operation', fallback_fn)
        
        result = await fallback_manager.execute_with_fallback(
            'test_operation', 
            primary_fn,
            'test_arg'
        )
        
        assert result['status'] == 'fallback_used'
        assert primary_fn.called
        assert fallback_fn.called
```

***

**Next:** [Rate Limits Guide](/api/guides/rate-limits.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://zaits.gitbook.io/api/guides/error-handling.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
