Middleware
Middleware are a mechanism to inspect requests — and associated responses — handled by your application. The purpose of middleware is broad and ranges from authentication to logging or CSRF protection. Expanse comes with a few built-in middleware that are ready to be used like exception handling or CORS management.
You can define your own middleware to add custom behavior to your application if needed, and you will see that it is a really easy process.
Creating your own middleware
Middleware are typically located in the app/http/middleware
directory, and, for convenience, you can
create a new middleware using the ./beam make middleware
command:
./beam make middleware validate_token
This will create a new validate_token.py
file in the app/http/middleware
directory.
A middleware is a class that defines a handle()
method which accepts a Request
instance
and a callable responsible for calling the next middleware in the stack.
from expanse.http.request import Request
from expanse.http.response import Response
from expanse.types.http.middleware import RequestHandler
class ValidateToken:
async def handle(
self, request: Request, next_call: RequestHandler
) -> Response:
return await next_call(request)
Within the handle()
method, a middleware can decide to proceed with the request by calling the
next_call()
callable,
finish the request by sending an early response or raise an exception to abort the request.
A request can go through any number of middleware before reaching the actual route corresponding to the request. Middleware are layers through which a request must go through and each of them can either accept, inspect or reject the request.
Every middleware will be resolved by the service container, so you can type-hint any dependencies in the constructor for them to be injected automatically.
Proceeding with the request
If the middleware should continue with the request, it should call the next_call()
callable with the
Request
instance received by the handle()
method. When creating a middleware, this will be the default behavior.
Aborting the request
If a middleware raises an exception, the request will not go further down the middleware stack and the underlying route handler will not be called. The exception will be caught by the global exception handler.
from expanse.http.request import Request
from expanse.http.response import Response
from expanse.http.helpers import abort
from expanse.types.http.middleware import RequestHandler
class ValidateToken:
async def handle(
self, request: Request, next_call: RequestHandler
) -> Response:
if request.headers["X-API-Token"] != "secret-token":
abort(403)
return await next_call(request)
Here the middleware checks if the X-API-Token
header is set to secret-token
and return a
403 Forbidden
HTTP
response if not.
If the token matches, it will simply pass the request to the next middleware by calling the
next_call()
callable
which expects a Request
object.
Sending an early response
The middleware can also decide to stop the request by sending an early response and not call next_call()
.
from expanse.http.request import Request
from expanse.http.response import Response
from expanse.types.http.middleware import RequestHandler
class ValidateToken:
async def handle(
self, request: Request, next_call: RequestHandler
) -> Response:
return Response("End of request")
Managing responses
While a middleware can perform actions before the request if send further down the middleware stack,
it can also act after the response corresponding to the request has been generated.
Any action that needs to be done after the request has been handled should be done after next_call()
has been called.
from expanse.http.request import Request
from expanse.http.response import Response
from expanse.types.http.middleware import RequestHandler
class AfterMiddleware:
async def handle(self, request: Request, next_call: RequestHandler) -> Response:
response: Response = await next_call(request)
# Do something
return response
Configuring middleware
Middleware can be configured at various levels: globally, per route, or per route group.
Global middleware
Global middleware run during any requests handled by your application, this type of middleware
is particularly useful for logging requests or exception handling. You can configure global middleware
in your app/app.py
file:
from expanse.core.application import Application
from expanse.core.http.middleware.middleware_stack import MiddlewareStack
async def configure_middleware(middleware: MiddlewareStack):
from app.http.middleware.validate_token import ValidateToken
middleware.append(ValidateToken)
app = Application.configure().with_middleware(configure_middleware).create()
The configuration of additional middleware is done through a configuration function that will be given
to the with_middleware()
method. This configuration function will be executed when the application is first
bootstrapped and will receive an instance of a MiddlewareStack
.
It's a good practice to import your additional middleware in the configuration function to avoid performance issues when configuring the application due to costly imports.
You can use the append()
method to add more middleware at the end of the stack, or
prepend
if you want to add them
to the beginning of the stack.
By default, every Expanse application has the following global middleware:
expanse.http.middleware.manage_cors.ManageCors
: this middleware is responsible for handling CORSOPTIONS
HTTP requests and set the correct headers on your behalf.