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.from expanse.http.middleware.trust_hosts.TrustHosts
: this middleware is responsible for configuring the trusted hosts for your application. This ensures that the application is not vulnerable to host header attacks.from expanse.http.middleware.trust_proxies.TrustProxies
: this middleware is responsible for configuring the trusted proxies for your application.
Middleware groups
You can also group middleware together under a common name. This is especially useful to assign a common set of middleware to routes without having to redeclare them every time. This is commonly done in the middleware configuration function.
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
from app.http.middleware.another_middleware import AnotherMiddleware
group = middleware.group("auth")
group.prepend(ValidateToken)
group.append(AnotherMiddleware)
app = Application.configure().with_middleware(configure_middleware).create()
Middleware groups can be assigned to routes or route groups using the middleware()
method.
from expanse.routing.router import Router
def routes(router: Router):
router.get("/", ...).middleware("auth")
with router.group("/admin", prefix="/admin") as admin:
admin.middleware("auth")
admin.get("/", ...)
Default middleware groups
By default, Expanse comes with a few middleware groups that you can use: web
and api
. These groups
are automatically applied to routes belonging to the web
and api
route groups respectively.
If you need to customize these middleware groups, you can do so in the middleware configuration function.
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
from app.http.middleware.my_web_middleware import MyWebMiddleware
middleware.group("web").prepend(MyWebMiddleware)
middleware.group("api").append(ValidateToken)
app = Application.configure().with_middleware(configure_middleware).create()
You can also replace one middleware of the group with another one by using the replace()
method.
from expanse.core.http.middleware.middleware_stack import MiddlewareStack
async def configure_middleware(middleware: MiddlewareStack):
from expanse.session.middleware.load_session import LoadSession
from app.http.middleware.custom_load_session import CustomLoadSession
middleware.group("web").replace(LoadSession, CustomLoadSession)
Finally, you may also remove a middleware from a group by using the remove()
method.
from expanse.core.http.middleware.middleware_stack import MiddlewareStack
async def configure_middleware(middleware: MiddlewareStack):
from expanse.session.middleware.load_session import LoadSession
middleware.group("web").remove(LoadSession)
The web
middleware group
The web
middleware group is applied to routes belonging to the web
route group. It includes the following
middleware:
expanse.http.middleware.encrypt_cookies.EncryptCookies
: this middleware is responsible for encrypting the cookies sent to the client and decrypt them when received from the client. See the corresponding section to know more about how to configure it.expanse.session.middleware.load_session.LoadSession
: this middleware is responsible for loading the session for the request and saving it after the response is sent.expanse.session.middleware.validate_csrf_token
: this middleware is responsible for validating the CSRF token.