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.

image image

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.

validate_token.py
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 CORS OPTIONS HTTP requests and set the correct headers on your behalf.