Error Handling and HTTP Exceptions

Robust error handling is crucial for any web application. The Lback Framework provides a structured way to manage exceptions, especially those related to HTTP responses, allowing you to return meaningful error messages and status codes to clients. This section details the core exception classes provided by the framework and how to use them.

1. Base Exception Classes

All framework-specific exceptions in Lback inherit from a common base, ensuring a consistent approach to error management.

from typing import Any, Dict, Optional, List

class FrameworkException(Exception):
    """
    Base exception for all framework-specific errors.
    All custom exceptions in the framework should inherit from this.
    """
    pass

class HTTPException(FrameworkException):
    """
    Base exception for HTTP-related errors.
    When raised, it typically results in an HTTP response with a specific status code.
    """
    status_code = 500
    message = "An unexpected error occurred."
    data: Optional[Any] = None

    def __init__(self, message: Optional[str] = None, status_code: Optional[int] = None, data: Optional[Any] = None):
        self.message = message if message is not None else self.__class__.message
        self.status_code = status_code if status_code is not None else self.__class__.status_code
        self.data = data if data is not None else self.__class__.data
        super().__init__(self.message)

FrameworkException:

This is the top-level exception for all custom errors originating from the Lback Framework. It provides a common parent for catching any framework-specific issues.

HTTPException:

This is the base class for all exceptions that should result in a specific HTTP response being sent back to the client. It includes attributes to define the HTTP status code, a message for the client, and optional additional data that can be included in the error response (e.g., validation errors).

  • status_code: The HTTP status code (e.g., 200, 400, 404, 500) associated with the error.

  • message: A human-readable message describing the error.

  • data: Optional extra data (e.g., a dictionary of validation errors) to be included in the error response payload.

You can raise HTTPException directly for generic HTTP errors, or use its specialized subclasses for more specific scenarios.

2. Common HTTP Exception Subclasses

The framework provides several pre-defined HTTPException subclasses for common HTTP error scenarios. Using these specific exceptions makes your code clearer and helps the framework handle responses appropriately.

class BadRequest(HTTPException):
    status_code = 400
    message = "Bad Request"

    def __init__(self, message: Optional[str] = None, data: Optional[Any] = None):
        super().__init__(message=message, status_code=400, data=data)
        if data is not None and message is None:
            self.message = "Validation failed." # Default message if data (errors) is provided

class NotFound(HTTPException):
    status_code = 404
    message = "Not Found"

    def __init__(self, message: Optional[str] = None, data: Optional[Any] = None):
        super().__init__(message=message, data=data)

class RouteNotFound(NotFound):
    message = "Route Not Found"

    def __init__(self, path: str, method: str, message: Optional[str] = None):
        super().__init__(message=message, data=None)
        self.path = path
        self.method = method
        if message is None:
            self.message = f"No route found for {method} {path}"

class Unauthorized(HTTPException):
    status_code = 401
    message = "Unauthorized"

    def __init__(self, message: Optional[str] = None, data: Optional[Any] = None):
        super().__init__(message=message, status_code=401, data=data)

class Forbidden(HTTPException):
    status_code = 403
    message = "Forbidden"

    def __init__(self, message: Optional[str] = None, data: Optional[Any] = None):
        super().__init__(message=message, status_code=403, data=data)

class MethodNotAllowed(HTTPException):
    status_code = 405
    message = "Method Not Allowed"

    def __init__(self, path: str, method: str, allowed_methods: list, message: Optional[str] = None):
        super().__init__(message=message, status_code=405, data=None)
        self.path = path
        self.method = method
        self.allowed_methods = allowed_methods
        if message is None:
            self.message = f"Method {method} not allowed for path {path}. Allowed methods: {', '.join(allowed_methods)}"

class ServerError(HTTPException):
    status_code = 500
    message = "Internal Server Error"

    def __init__(self, message: Optional[str] = None, data: Optional[Any] = None):
        super().__init__(message=message, status_code=500, data=data)

class ConfigurationError(FrameworkException):
    """
    Raised when there's an issue with the application's configuration.
    """
    pass

class ValidationError(BadRequest): # Inherits from BadRequest (HTTP 400)
    status_code = 400
    message = "Validation Error"

    def __init__(self, errors: Dict[str, List[str]], message: Optional[str] = None):
        super().__init__(message=message, status_code=400, data=errors)
        if self.message is None:
            self.message = "Validation failed."
        self.errors = errors # Specific attribute to hold validation details

Here’s a breakdown of the commonly used HTTP exception classes:

  • BadRequest(message: Optional[str], data: Optional[Any]) (Status: 400 Bad Request):

    Used when the server cannot process the request due to client error (e.g., malformed syntax, invalid request parameters). If data is provided (e.g., a dictionary of validation errors), the default message will be “Validation failed.”.

  • NotFound(message: Optional[str], data: Optional[Any]) (Status: 404 Not Found):

    Indicates that the requested resource could not be found on the server.

  • RouteNotFound(path: str, method: str, message: Optional[str]) (Status: 404 Not Found):

    A specialized NotFound exception, automatically populated with details about the requested path and method when no matching route is found.

  • Unauthorized(message: Optional[str], data: Optional[Any]) (Status: 401 Unauthorized):

    Signifies that authentication is required or has failed. This typically means the client needs to provide valid authentication credentials.

  • Forbidden(message: Optional[str], data: Optional[Any]) (Status: 403 Forbidden):

    Indicates that the server understood the request but refuses to authorize it. This implies that the client’s credentials (if any) are valid, but they do not have permission to access the resource.

  • MethodNotAllowed(path: str, method: str, allowed_methods: list, message: Optional[str]) (Status: 405 Method Not Allowed):

    Raised when the HTTP method used in the request (e.g., POST) is not supported for the resource identified by the URL. It automatically includes the allowed methods.

  • ServerError(message: Optional[str], data: Optional[Any]) (Status: 500 Internal Server Error):

    A general-purpose error message, used when an unexpected condition was encountered on the server. This should ideally be caught and handled, but serves as a fallback for uncaught exceptions.

  • ConfigurationError:

    This FrameworkException (not an HTTPException) is specifically for issues related to the application’s setup or settings.

  • ValidationError(errors: Dict[str, List[str]], message: Optional[str]) (Status: 400 Bad Request):

    A crucial exception for data validation. It inherits from BadRequest and is specifically designed to carry detailed validation error messages, typically as a dictionary where keys are field names and values are lists of error strings for that field.

3. Raising and Handling Exceptions

You can raise these exceptions directly within your views, middlewares, or utility functions. The framework’s core request handling mechanism is designed to catch HTTPException instances and convert them into appropriate HTTP responses.

Example: Raising a BadRequest for invalid input

from lback.exceptions import BadRequest
from lback.utils.validation import validate_json, ValidationError # Assuming your validation utilities are imported

def create_user_view(request):
    try:
        # Attempt to validate incoming JSON data
        user_data = validate_json(
            request,
            required_fields={"username": str, "email": str, "password": str},
            optional_fields={"age": int}
        )
        # If validation passes, proceed with business logic
        # ... create user in database ...
        return Response(json={"message": "User created successfully"}, status_code=201)

    except ValidationError as e:
        # If validate_json raises ValidationError, it means the data is invalid.
        # ValidationError itself is a BadRequest, so you can re-raise it,
        # and the framework will handle it as a 400 with the error data.
        raise e # Or you could catch it and return a custom Response
    except Exception as e:
        # Catch any other unexpected errors and raise a 500
        raise ServerError(message="Failed to create user due to server issue.")

Example: Raising NotFound for missing resources

from lback.exceptions import NotFound

def get_user_detail_view(request, user_id):
    user = get_user_from_db(user_id) # Assume this function fetches a user
    if user is None:
        raise NotFound(message=f"User with ID {user_id} not found.")
    return Response(json=user.to_dict())

Framework’s Default Error Handling:

By default, when an HTTPException is raised:

  • The framework intercepts it.

  • It extracts the status_code, message, and data (if any) from the exception.

  • It constructs an HTTP response with the specified status code and typically a JSON body containing {"detail": "Your error message here", "data": {...}}.

  • This response then proceeds through the process_response middleware chain.

For unhandled Python exceptions (not instances of HTTPException), the framework will typically convert them into a ServerError (500 Internal Server Error) response, logging the full traceback for debugging purposes (especially when DEBUG is enabled).

By leveraging these exception classes, you can provide clear, standardized error responses to clients, making your API more robust and easier to consume.