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
MIDDLEWARESsetting.process_response: Methods are executed in the reverse order of their listing in your
MIDDLEWARESsetting.
—
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 likerequest.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
Responseobject. When aResponseis returned, the request processing chain is short-circuited, and no furtherprocess_requestmethods or the view function will be called. Instead, the returnedResponsewill immediately proceed to theprocess_responsephase (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 earlierprocess_requestmethods).response: TheResponseobject 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-exceptblocks. Unhandled exceptions in middleware will result in a 500 Internal Server Error.- Order Matters: The order of middlewares in your
MIDDLEWARESlist is crucial. Middlewares that need to modify the request before others (e.g.,
SessionMiddlewarebeforeAuthMiddleware) should come earlier.Middlewares that need to process the final response (e.g.,
SecurityHeadersMiddleware,TimerMiddleware) might come later, asprocess_responseruns in reverse order.
- Order Matters: The order of middlewares in your
Avoid Heavy Operations in `process_request` if possible: If a middleware performs a very long-running operation in
process_requestand doesn’t short-circuit, it will delay every request. Consider if the operation can be deferred or moved toprocess_responseor 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_requestreturns eitherNoneor aResponseobject, andprocess_responsealways returns aResponseobject. Returning incorrect types can lead to unexpected behavior or errors.Don’t Forget
super()Calls (if overridingBaseMiddlewaremethods): If you override__init__in your middleware and inherit fromBaseMiddleware, remember to callsuper().__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.