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.

Middleware workflow Middleware workflow

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 CORS OPTIONS 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.