Custom Middlewares

The Lback Framework’s Middleware System provides a powerful mechanism to hook into the request and response processing pipeline. Middlewares are reusable components that can perform actions before a request reaches your view, or after a response is generated by your view. This allows for cross-cutting concerns like authentication, logging, security, and session management to be handled cleanly and efficiently.

1. Understanding Middleware

A middleware is essentially a callable object (typically an instance of a class) that sits between the web server and your application’s views. It has the opportunity to:

  • Process Incoming Requests: Intercept a request before it’s routed to a view. It can modify the request, perform checks (e.g., authentication, rate limiting), or even return a response directly, short-circuiting the entire request-response cycle.

  • Process Outgoing Responses: Intercept a response after it’s generated by a view (or by an earlier middleware). It can modify the response (e.g., add headers, compress content), or perform post-processing tasks (e.g., logging response times).

The framework processes middlewares in a specific order:

  • process_request: Methods are executed in the order they are listed in your MIDDLEWARES setting.

  • process_response: Methods are executed in the reverse order of their listing in your MIDDLEWARES setting.

2. The Middleware Protocol

To create a custom middleware, your class must adhere to the Middleware Protocol, which defines two essential methods: process_request and process_response. While you can implement these methods directly, it’s recommended to inherit from lback.core.base_middleware.BaseMiddleware for consistency and to leverage any base functionality.

# lback/core/base_middleware.py (Simplified for illustration)
from typing import Optional, Any, Protocol, runtime_checkable
from lback.core.response import Response

@runtime_checkable
class Middleware(Protocol):
    def process_request(self, request: Any) -> Optional['Response']:
        """
        Process an incoming request before the view is dispatched.
        Can modify the request or return a Response to short-circuit the request processing chain.
        """
        pass

    def process_response(self, request: Any, response: 'Response') -> 'Response':
        """
        Process the outgoing response after the view has been executed.
        Can modify the response. This method is called in reverse order of middleware addition.
        """
        pass

class BaseMiddleware(Middleware):
    """
    Base class for all middlewares. Provides default implementations
    of process_request and process_response that simply pass through.
    """
    def __init__(self, **kwargs):
        # Base middlewares might accept common dependencies or parameters
        pass

    def process_request(self, request: Any) -> Optional[Response]:
        # Default: Do nothing and pass the request along
        return None

    def process_response(self, request: Any, response: Response) -> Response:
        # Default: Do nothing and pass the response along
        return response

process_request(self, request)

  • Purpose: Executed when a request first enters the middleware chain.

  • Arguments:
    • request: The incoming request object. You can modify this object (e.g., add attributes like request.user, request.session).

  • Return Value:
    • None: The most common return. Indicates that the middleware has processed the request and the request should continue to the next middleware in the chain, or finally to the view.

    • Response object: If the middleware decides to handle the request completely (e.g., redirect an unauthenticated user, block a malicious request), it can return a Response object. When a Response is returned, the request processing chain is short-circuited, and no further process_request methods or the view function will be called. Instead, the returned Response will immediately proceed to the process_response phase (starting from the current middleware backwards).

process_response(self, request, response)

  • Purpose: Executed after a response has been generated by the view (or by a short-circuiting middleware).

  • Arguments:
    • request: The original request object (potentially modified by earlier process_request methods).

    • response: The Response object generated by the view or a preceding middleware.

  • Return Value:
    • Must always return a ``Response`` object. You can modify the response object (e.g., add headers, change content) and then return it. This method is called in reverse order of middleware definition.

3. Creating a Custom Middleware

To create your own middleware, define a class that inherits from BaseMiddleware (located at lback.core.base_middleware.BaseMiddleware) and implement the process_request and/or process_response methods.

Example: A Simple Timer Middleware

Let’s create a middleware that logs the time taken to process each request.

# myapp/middlewares.py
import time
import logging
from typing import Any, Optional
from lback.core.response import Response
from lback.core.base_middleware import BaseMiddleware

logger = logging.getLogger(__name__)

class RequestTimerMiddleware(BaseMiddleware):
    """
    Logs the time taken to process each request.
    """
    def process_request(self, request: Any) -> Optional[Response]:
        """
        Records the start time of the request.
        """
        request.start_time = time.time()
        logger.debug(f"RequestTimerMiddleware: Request started at {request.start_time}")
        return None # Continue processing the request

    def process_response(self, request: Any, response: Response) -> Response:
        """
        Calculates and logs the time taken for the request.
        """
        if hasattr(request, 'start_time'):
            end_time = time.time()
            process_time = end_time - request.start_time
            logger.info(f"Request: {request.method} {request.path} processed in {process_time:.4f} seconds.")
        else:
            logger.warning("RequestTimerMiddleware: start_time not found on request.")
        return response # Always return the response

Accessing Dependencies in Middleware

Your framework’s middleware loader (lback.core.middleware_loader.create_middleware) automatically injects dependencies into your middleware’s __init__ method if they are available in the application’s context (e.g., config, db_session, router, template_renderer).

Example: Middleware Accessing Config

# myapp/middlewares.py
import logging
from typing import Any, Optional
from lback.core.response import Response
from lback.core.base_middleware import BaseMiddleware
from lback.core.config import Config # Import the Config type hint

logger = logging.getLogger(__name__)

class CustomHeaderMiddleware(BaseMiddleware):
    """
    Adds a custom header to all responses, configurable via settings.
    """
    def __init__(self, config: Config): # The 'config' dependency is automatically injected
        self.config = config
        self.custom_header_value = getattr(self.config, 'APP_CUSTOM_HEADER', 'DefaultAppValue')
        logger.info(f"CustomHeaderMiddleware initialized with value: {self.custom_header_value}")

    def process_response(self, request: Any, response: Response) -> Response:
        response.headers['X-Framework-Custom-Header'] = self.custom_header_value
        logger.debug(f"Added custom header to response for {request.path}")
        return response

4. Registering Your Custom Middleware

After creating your middleware class, you need to register it in your application’s MIDDLEWARES setting, typically found in your settings.py (or Config class). The order in this list matters significantly for the execution flow.

You can register middlewares as a string (the full import path to the class) or as a dictionary if you need to pass specific parameters to its constructor.

Example: Adding to MIDDLEWARES setting

# settings.py (or your Config class)

MIDDLEWARES = [
    # Core middlewares (usually come first)
    "lback.middlewares.sqlalchemy_middleware.SQLAlchemySessionMiddleware",
    "lback.middlewares.static_files_middleware.StaticFilesMiddleware",
    {
        "class": "lback.middlewares.session_middleware.SessionMiddleware",
        "params": {} # Example: no specific params for this middleware
    },
    # ... other built-in middlewares ...

    # Your custom middlewares
    "myapp.middlewares.RequestTimerMiddleware", # Registered as a string
    {
        "class": "myapp.middlewares.CustomHeaderMiddleware",
        "params": {
            # Parameters passed to CustomHeaderMiddleware's __init__
            # Note: 'config' is automatically injected, 'params' are extra args
            # "some_extra_param": "value_from_settings"
        }
    },
    # ...
]

# Example of a custom setting used by CustomHeaderMiddleware
APP_CUSTOM_HEADER = "MyAwesomeApp"

5. Best Practices for Writing Middlewares

  • Keep it Focused: Each middleware should ideally have a single, clear responsibility (e.g., authentication, logging, security header management).

  • Handle Exceptions: If your middleware performs operations that might raise exceptions (e.g., database queries, external API calls), wrap them in try-except blocks. Unhandled exceptions in middleware will result in a 500 Internal Server Error.

  • Order Matters: The order of middlewares in your MIDDLEWARES list is crucial.
    • Middlewares that need to modify the request before others (e.g., SessionMiddleware before AuthMiddleware) should come earlier.

    • Middlewares that need to process the final response (e.g., SecurityHeadersMiddleware, TimerMiddleware) might come later, as process_response runs in reverse order.

  • Avoid Heavy Operations in `process_request` if possible: If a middleware performs a very long-running operation in process_request and doesn’t short-circuit, it will delay every request. Consider if the operation can be deferred or moved to process_response or an asynchronous task if performance is critical.

  • Use Logging: Log important events, errors, and debugging information within your middleware to aid in troubleshooting.

  • Return Correct Types: Always ensure process_request returns either None or a Response object, and process_response always returns a Response object. Returning incorrect types can lead to unexpected behavior or errors.

  • Don’t Forget super() Calls (if overriding BaseMiddleware methods): If you override __init__ in your middleware and inherit from BaseMiddleware, remember to call super().__init__(**kwargs) to ensure the base class is properly initialized.

By following these guidelines, you can effectively extend your Lback application’s functionality and manage its core behavior through custom middlewares.