Skip to content
Last updated

Comprehensive guide to understanding, handling, and recovering from errors when using the Freddy API.

📋 Error Response Format

All Freddy API errors follow a consistent JSON format:

{
  "success": false,
  "error": {
    "code": "INVALID_EMAIL_FORMAT",
    "message": "Please enter a valid email address.",
    "system_message": "Email validation failed",
    "type": "client_error",
    "status": 422,
    "details": {
      "field": "email",
      "value": "invalid-email"
    },
    "trace_id": "2fbbf3b6-51a1-4f1b-88e2-c00e8b52fbb8",
    "timestamp": "2025-01-15T10:30:00Z"
  }
}

Error Response Fields

  • success: Always false for error responses
  • error: Error object containing all error details
    • code: Machine-readable error code (e.g., INVALID_EMAIL_FORMAT)
    • message: User-friendly error message safe to display in UI
    • system_message: Technical error message for developers/logs
    • type: Error category (e.g., client_error, server_error)
    • status: HTTP status code
    • details: Additional context information (optional)
    • trace_id: Unique identifier for tracking and debugging
    • timestamp: ISO 8601 timestamp when error occurred

🚦 HTTP Status Codes

2xx Success Codes

CodeStatusDescription
200OKRequest successful
201CreatedResource created successfully
204No ContentRequest successful, no response body

4xx Client Error Codes

CodeStatusDescriptionAction Required
400Bad RequestInvalid request format or parametersFix request data
401UnauthorizedInvalid or missing API keyCheck authentication
403ForbiddenInsufficient permissionsCheck account permissions
404Not FoundResource doesn't existVerify resource ID
409ConflictResource already existsUse different identifier
422Unprocessable EntityValidation failedFix validation errors
429Too Many RequestsRate limit exceededImplement backoff

5xx Server Error Codes

CodeStatusDescriptionAction Required
500Internal Server ErrorServer-side errorRetry with backoff
502Bad GatewayGateway errorRetry with backoff
503Service UnavailableService temporarily downRetry later
504Gateway TimeoutRequest timeoutRetry with longer timeout

🔍 Error Types

Authentication Errors

AuthenticationException (401)

{
  "success": false,
  "error": {
    "code": "INVALID_CREDENTIALS",
    "message": "Invalid email, username, or password. Please try again.",
    "system_message": "Authentication failed",
    "type": "authentication_error",
    "status": 401,
    "details": {
      "email": "user@example.com"
    },
    "trace_id": "2fbbf3b6-51a1-4f1b-88e2-c00e8b52fbb8",
    "timestamp": "2025-01-15T10:30:00Z"
  }
}

Common Error Codes:

  • INVALID_CREDENTIALS - Wrong email/password
  • TOKEN_EXPIRED - Session expired
  • TOKEN_INVALID - Malformed token
  • AUTHENTICATION_REQUIRED - Missing authentication
  • INVALID_API_KEY - Invalid API key
  • API_KEY_EXPIRED - API key expired
  • API_KEY_DEACTIVATED - API key deactivated

Common Causes:

  • Missing Authorization header or api-key header
  • Invalid API key format
  • Expired or revoked API key/token
  • API key for wrong environment

Resolution:

def handle_auth_error(response):
    if response.status_code == 401:
        error_data = response.json()
        error_code = error_data.get("error", {}).get("code")
        
        if error_code == "INVALID_API_KEY":
            # Check API key configuration
            api_key = os.getenv('FREDDY_API_KEY')
            if not api_key:
                raise ValueError("FREDDY_API_KEY environment variable not set")
            # Key might be expired - generate new one
            print("API key may be expired. Please generate a new key.")
        elif error_code == "TOKEN_EXPIRED":
            # Refresh the token
            print("Token expired. Please refresh your session.")

Validation Errors

ValidationException (422)

{
  "success": false,
  "error": {
    "code": "INVALID_EMAIL_FORMAT",
    "message": "Please enter a valid email address.",
    "system_message": "Email validation failed",
    "type": "validation_error",
    "status": 422,
    "details": {
      "field": "email",
      "value": "invalid-email"
    },
    "trace_id": "2fbbf3b6-51a1-4f1b-88e2-c00e8b52fbb8",
    "timestamp": "2025-01-15T10:30:00Z"
  }
}

Common Error Codes:

  • VALIDATION_ERROR - General validation failure
  • INVALID_EMAIL_FORMAT - Invalid email format
  • INVALID_PASSWORD_FORMAT - Password doesn't meet requirements
  • MISSING_REQUIRED_FIELD - Required field missing
  • INVALID_FIELD_VALUE - Field contains invalid value
  • INVALID_USERNAME_FORMAT - Username format invalid
  • RULE_CONTENT_TOO_LARGE - Content exceeds maximum size
  • RULE_CONTENT_EMPTY - Content cannot be empty

Handling Validation Errors:

def handle_validation_errors(response):
    if response.status_code == 422:
        error_data = response.json()
        error = error_data.get("error", {})
        
        code = error.get("code")
        message = error.get("message")
        details = error.get("details", {})
        
        print(f"Validation error ({code}): {message}")
        if "field" in details:
            print(f"Field: {details['field']}")
        
        return error
    return None

Rate Limit Errors

RateLimitException (429)

{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests. Please wait a moment and try again.",
    "system_message": "Rate limit exceeded",
    "type": "rate_limit_error",
    "status": 429,
    "details": {
      "retry_after": 60
    },
    "trace_id": "2fbbf3b6-51a1-4f1b-88e2-c00e8b52fbb8",
    "timestamp": "2025-01-15T10:30:00Z"
  }
}

Common Error Codes:

  • RATE_LIMIT_EXCEEDED - General rate limit exceeded
  • TOO_MANY_LOGIN_ATTEMPTS - Too many login attempts
  • TOO_MANY_VERIFICATION_ATTEMPTS - Too many verification attempts
  • SPENDING_LIMIT_EXCEEDED - Monthly spending limit reached

Rate Limit Headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200
Retry-After: 60

Handling Rate Limits:

import time
from datetime import datetime

def handle_rate_limit(response):
    if response.status_code == 429:
        error_data = response.json()
        error = error_data.get("error", {})
        details = error.get("details", {})
        
        # Get retry delay from details or header
        retry_after = details.get('retry_after')
        if not retry_after:
            retry_after = response.headers.get('Retry-After', 60)
        
        retry_after = int(retry_after)
        print(f"Rate limited. Waiting {retry_after} seconds...")
        time.sleep(retry_after)
        return True
    return False

Authorization Errors

AuthorizationException (403)

{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_PERMISSIONS",
    "message": "You don't have permission to perform this action.",
    "system_message": "User lacks required permissions",
    "type": "authorization_error",
    "status": 403,
    "details": {
      "required_role": "admin"
    },
    "trace_id": "2fbbf3b6-51a1-4f1b-88e2-c00e8b52fbb8",
    "timestamp": "2025-01-15T10:30:00Z"
  }
}

Common Error Codes:

  • INSUFFICIENT_PERMISSIONS - Lacks required permissions
  • ORGANIZATION_ACCESS_DENIED - No access to organization
  • DOMAIN_NOT_REGISTERED - Email domain not registered
  • REGISTRATION_RESTRICTED - Registration not allowed
  • ACCESS_DENIED - General access denied
  • CANNOT_REMOVE_LAST_OWNER - Cannot remove last owner

Common Causes:

  • API key lacks required permissions
  • Trying to access another user's resources
  • Organization-level restrictions
  • Feature not available in current plan

Resource Errors

ResourceNotFoundException (404)

{
  "success": false,
  "error": {
    "code": "THREAD_NOT_FOUND",
    "message": "The requested resource could not be found.",
    "system_message": "Thread not found",
    "type": "client_error",
    "status": 404,
    "details": {
      "thread_id": "thread_abc123"
    },
    "trace_id": "2fbbf3b6-51a1-4f1b-88e2-c00e8b52fbb8",
    "timestamp": "2025-01-15T10:30:00Z"
  }
}

Common Error Codes:

  • USER_NOT_FOUND - User doesn't exist
  • ORGANIZATION_NOT_FOUND - Organization doesn't exist
  • RESOURCE_NOT_FOUND - Generic resource not found
  • THREAD_NOT_FOUND - Thread doesn't exist
  • MESSAGE_NOT_FOUND - Message doesn't exist
  • RULE_NOT_FOUND - Rule doesn't exist
  • ASSISTANT_NOT_FOUND - Assistant doesn't exist

ConflictException (409)

{
  "success": false,
  "error": {
    "code": "USER_ALREADY_EXISTS",
    "message": "An account with this email already exists.",
    "system_message": "Email already registered",
    "type": "client_error",
    "status": 409,
    "details": {
      "email": "user@example.com"
    },
    "trace_id": "2fbbf3b6-51a1-4f1b-88e2-c00e8b52fbb8",
    "timestamp": "2025-01-15T10:30:00Z"
  }
}

Common Error Codes:

  • USER_ALREADY_EXISTS - Email already registered
  • USERNAME_TAKEN - Username already taken
  • RESOURCE_ALREADY_EXISTS - Resource already exists
  • RULE_NAME_DUPLICATE - Rule name already exists
  • CONFLICT - Generic conflict

Server Errors

InternalServerException (500)

{
  "success": false,
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "Something went wrong. Please try again later.",
    "system_message": "Internal server error",
    "type": "server_error",
    "status": 500,
    "trace_id": "2fbbf3b6-51a1-4f1b-88e2-c00e8b52fbb8",
    "timestamp": "2025-01-15T10:30:00Z"
  }
}

Common Error Codes:

  • INTERNAL_ERROR - Unexpected server error
  • DATABASE_ERROR - Database operation failed
  • EMAIL_SEND_FAILED - Email service failure
  • CONFIGURATION_ERROR - Service configuration error
  • STORAGE_SERVICE_UNAVAILABLE - Storage service down
  • PROVIDER_UNAVAILABLE - External service unavailable
  • PROVIDER_TIMEOUT - External service timeout

Handling Server Errors: Server errors (5xx) are typically temporary and should be retried with exponential backoff. Always include the trace_id when reporting issues to support.

🔄 Retry Strategies

Exponential Backoff

import time
import random
from typing import Callable, Any

def exponential_backoff_retry(
    func: Callable,
    max_retries: int = 3,
    base_delay: float = 1.0,
    max_delay: float = 60.0,
    backoff_factor: float = 2.0,
    jitter: bool = True
) -> Any:
    """
    Retry function with exponential backoff.
    
    Args:
        func: Function to retry
        max_retries: Maximum number of retry attempts
        base_delay: Initial delay in seconds
        max_delay: Maximum delay in seconds
        backoff_factor: Multiplier for delay after each retry
        jitter: Add random jitter to prevent thundering herd
    """
    
    for attempt in range(max_retries + 1):
        try:
            return func()
        except Exception as e:
            if attempt == max_retries:
                raise e
            
            # Calculate delay
            delay = min(base_delay * (backoff_factor ** attempt), max_delay)
            
            # Add jitter
            if jitter:
                delay += random.uniform(0, delay * 0.1)
            
            print(f"Attempt {attempt + 1} failed: {e}. Retrying in {delay:.2f}s...")
            time.sleep(delay)

# Usage
def make_api_request():
    response = requests.get(
        "https://api.freddy.ai/v2/models",
        headers={"api-key": os.getenv('FREDDY_API_KEY')}
    )
    response.raise_for_status()
    return response.json()

result = exponential_backoff_retry(make_api_request)

Smart Retry Logic

class SmartRetryClient:
    def __init__(self, api_key: str):
        self.api_key = api_key
        self.headers = {"api-key": api_key}
    
    def should_retry(self, response) -> bool:
        """Determine if request should be retried."""
        # Always retry on server errors
        if response.status_code >= 500:
            return True
        
        # Retry on rate limits
        if response.status_code == 429:
            return True
        
        # Don't retry on client errors (except rate limits)
        if 400 <= response.status_code < 500:
            return False
        
        return False
    
    def get_retry_delay(self, response, attempt: int) -> float:
        """Calculate retry delay based on response."""
        if response.status_code == 429:
            # Use Retry-After header if available
            retry_after = response.headers.get('Retry-After')
            if retry_after:
                return float(retry_after)
        
        # Exponential backoff for server errors
        return min(2 ** attempt, 60)  # Cap at 60 seconds
    
    def make_request(self, method: str, endpoint: str, **kwargs) -> dict:
        """Make API request with smart retry logic."""
        url = f"https://api.freddy.ai/v2/{endpoint}"
        max_retries = 3
        
        for attempt in range(max_retries + 1):
            try:
                response = requests.request(
                    method, url, headers=self.headers, **kwargs
                )
                
                # Success
                if response.status_code < 400:
                    return response.json()
                
                # Check if we should retry
                if attempt < max_retries and self.should_retry(response):
                    delay = self.get_retry_delay(response, attempt)
                    print(f"Request failed (attempt {attempt + 1}). Retrying in {delay}s...")
                    time.sleep(delay)
                    continue
                
                # No more retries or shouldn't retry
                response.raise_for_status()
                
            except requests.exceptions.RequestException as e:
                if attempt < max_retries:
                    delay = 2 ** attempt
                    print(f"Network error (attempt {attempt + 1}): {e}. Retrying in {delay}s...")
                    time.sleep(delay)
                    continue
                raise
        
        raise Exception("Max retries exceeded")

# Usage
client = SmartRetryClient(os.getenv('FREDDY_API_KEY'))
result = client.make_request("GET", "models")

🛡️ Comprehensive Error Handler

import logging
from enum import Enum
from dataclasses import dataclass
from typing import Optional, Dict, Any

class ErrorSeverity(Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"

@dataclass
class ErrorContext:
    endpoint: str
    method: str
    request_data: Optional[Dict[str, Any]] = None
    user_id: Optional[str] = None
    organization_id: Optional[str] = None

class FreddyErrorHandler:
    def __init__(self, logger: Optional[logging.Logger] = None):
        self.logger = logger or logging.getLogger(__name__)
    
    def handle_error(self, response, context: ErrorContext) -> Dict[str, Any]:
        """Comprehensive error handling."""
        try:
            error_data = response.json()
        except:
            error_data = {
                "success": False,
                "error": {
                    "code": "UNKNOWN_ERROR",
                    "message": "Unknown error",
                    "status": response.status_code
                }
            }
        
        error = error_data.get("error", {})
        
        error_info = {
            "status_code": response.status_code,
            "error_code": error.get("code", "UNKNOWN_ERROR"),
            "error_type": error.get("type", "unknown_error"),
            "message": error.get("message", "Unknown error"),
            "system_message": error.get("system_message", "Unknown error"),
            "details": error.get("details", {}),
            "trace_id": error.get("trace_id"),
            "timestamp": error.get("timestamp"),
            "context": context,
            "severity": self._determine_severity(response.status_code),
            "recoverable": self._is_recoverable(response.status_code),
            "suggested_action": self._get_suggested_action(response.status_code, error)
        }
        
        # Log the error
        self._log_error(error_info)
        
        # Handle specific error types
        if response.status_code == 401:
            return self._handle_auth_error(error_info)
        elif response.status_code == 403:
            return self._handle_permission_error(error_info)
        elif response.status_code == 429:
            return self._handle_rate_limit_error(error_info, response)
        elif response.status_code == 422:
            return self._handle_validation_error(error_info, error_data)
        elif response.status_code >= 500:
            return self._handle_server_error(error_info)
        else:
            return self._handle_client_error(error_info)
    
    def _determine_severity(self, status_code: int) -> ErrorSeverity:
        """Determine error severity based on status code."""
        if status_code >= 500:
            return ErrorSeverity.HIGH
        elif status_code == 429:
            return ErrorSeverity.MEDIUM
        elif status_code in [401, 403]:
            return ErrorSeverity.MEDIUM
        else:
            return ErrorSeverity.LOW
    
    def _is_recoverable(self, status_code: int) -> bool:
        """Determine if error is recoverable."""
        # Server errors and rate limits are recoverable
        if status_code >= 500 or status_code == 429:
            return True
        # Auth errors might be recoverable (refresh token, new API key)
        if status_code in [401, 403]:
            return True
        # Most client errors are not recoverable
        return False
    
    def _get_suggested_action(self, status_code: int, error: Dict) -> str:
        """Get suggested action for error."""
        error_code = error.get("code", "")
        
        if status_code == 401:
            if "API_KEY" in error_code:
                return "Check API key validity and regenerate if necessary"
            elif "TOKEN" in error_code:
                return "Refresh authentication token or sign in again"
            return "Check authentication credentials"
        elif status_code == 403:
            return "Verify account permissions and upgrade plan if needed"
        elif status_code == 429:
            return "Implement rate limiting and retry with exponential backoff"
        elif status_code == 422:
            return "Fix validation errors in request data"
        elif status_code == 404:
            return "Verify resource ID exists and is accessible"
        elif status_code >= 500:
            return "Retry request with exponential backoff"
        else:
            return "Review request format and parameters"
    
    def _log_error(self, error_info: Dict[str, Any]):
        """Log error with appropriate level."""
        severity = error_info["severity"]
        message = f"API Error: {error_info['message']}"
        
        extra = {
            "status_code": error_info["status_code"],
            "error_code": error_info["error_code"],
            "error_type": error_info["error_type"],
            "trace_id": error_info["trace_id"],
            "endpoint": error_info["context"].endpoint,
            "method": error_info["context"].method,
            "recoverable": error_info["recoverable"]
        }
        
        if severity == ErrorSeverity.CRITICAL:
            self.logger.critical(message, extra=extra)
        elif severity == ErrorSeverity.HIGH:
            self.logger.error(message, extra=extra)
        elif severity == ErrorSeverity.MEDIUM:
            self.logger.warning(message, extra=extra)
        else:
            self.logger.info(message, extra=extra)
    
    def _handle_auth_error(self, error_info: Dict) -> Dict[str, Any]:
        """Handle authentication errors."""
        return {
            **error_info,
            "recovery_steps": [
                "Verify API key is set correctly",
                "Check API key format (should start with 'ak_')",
                "Generate new API key if current one is expired",
                "Ensure using correct environment (dev/prod)"
            ]
        }
    
    def _handle_rate_limit_error(self, error_info: Dict, response) -> Dict[str, Any]:
        """Handle rate limit errors."""
        details = error_info.get("details", {})
        retry_after = details.get('retry_after') or response.headers.get('Retry-After', '60')
        
        return {
            **error_info,
            "retry_after": int(retry_after),
            "recovery_steps": [
                f"Wait {retry_after} seconds before retrying",
                "Implement exponential backoff",
                "Consider upgrading to higher rate limit plan",
                "Optimize request patterns to reduce frequency"
            ]
        }
    
    def _handle_validation_error(self, error_info: Dict, error: Dict) -> Dict[str, Any]:
        """Handle validation errors."""
        details = error_info.get("details", {})
        
        return {
            **error_info,
            "validation_details": details,
            "recovery_steps": [
                "Fix validation errors in request data",
                "Check API documentation for required fields",
                "Validate data format and types",
                "Ensure all required parameters are provided"
            ]
        }
    
    def _handle_server_error(self, error_info: Dict) -> Dict[str, Any]:
        """Handle server errors."""
        return {
            **error_info,
            "recovery_steps": [
                "Retry request with exponential backoff",
                "Check API status page for service issues",
                "Contact support if error persists",
                "Implement circuit breaker pattern"
            ]
        }
    
    def _handle_client_error(self, error_info: Dict) -> Dict[str, Any]:
        """Handle other client errors."""
        return {
            **error_info,
            "recovery_steps": [
                "Review request format and parameters",
                "Check API documentation for endpoint requirements",
                "Verify resource IDs and permissions",
                "Ensure request data is properly formatted"
            ]
        }

# Usage
error_handler = FreddyErrorHandler()

def make_api_request_with_error_handling(endpoint, method="GET", **kwargs):
    context = ErrorContext(
        endpoint=endpoint,
        method=method,
        request_data=kwargs.get("json"),
        user_id="user_123"  # Get from session
    )
    
    try:
        response = requests.request(
            method,
            f"https://api.freddy.ai/v2/{endpoint}",
            headers={"api-key": os.getenv('FREDDY_API_KEY')},
            **kwargs
        )
        
        if response.status_code >= 400:
            error_info = error_handler.handle_error(response, context)
            
            # Handle based on error type
            if error_info["recoverable"] and error_info["status_code"] == 429:
                time.sleep(error_info.get("retry_after", 60))
                # Retry the request
                return make_api_request_with_error_handling(endpoint, method, **kwargs)
            
            raise Exception(f"API Error ({error_info['error_code']}): {error_info['message']}")
        
        return response.json()
        
    except requests.exceptions.RequestException as e:
        context_dict = {
            "endpoint": endpoint,
            "method": method,
            "error": str(e)
        }
        error_handler.logger.error("Network error", extra=context_dict)
        raise

📚 Next Steps


Handle errors gracefully and build resilient applications! 🚨