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. .. code-block:: python # 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. .. code-block:: python # 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`` .. code-block:: python # 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** .. code-block:: python # 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.