Skip to content

Middleware

Starlette includes several middleware classes for adding behavior that is applied across your entire application. These are all implemented as standard ASGI middleware classes, and can be applied either to Starlette or to any other ASGI application.

Using middleware

The Starlette application class allows you to include the ASGI middleware in a way that ensures that it remains wrapped by the exception handler.

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware

routes = ...

# Ensure that all requests include an 'example.com' or
# '*.example.com' host header, and strictly enforce https-only access.
middleware = [
    Middleware(
        TrustedHostMiddleware,
        allowed_hosts=['example.com', '*.example.com'],
    ),
    Middleware(HTTPSRedirectMiddleware)
]

app = Starlette(routes=routes, middleware=middleware)

Every Starlette application automatically includes two pieces of middleware by default:

  • ServerErrorMiddleware - Ensures that application exceptions may return a custom 500 page, or display an application traceback in DEBUG mode. This is always the outermost middleware layer.
  • ExceptionMiddleware - Adds exception handlers, so that particular types of expected exception cases can be associated with handler functions. For example raising HTTPException(status_code=404) within an endpoint will end up rendering a custom 404 page.

Middleware is evaluated from top-to-bottom, so the flow of execution in our example application would look like this:

  • Middleware
    • ServerErrorMiddleware
    • TrustedHostMiddleware
    • HTTPSRedirectMiddleware
    • ExceptionMiddleware
  • Routing
  • Endpoint

The following middleware implementations are available in the Starlette package:

CORSMiddleware

Adds appropriate CORS headers to outgoing responses in order to allow cross-origin requests from browsers.

The default parameters used by the CORSMiddleware implementation are restrictive by default, so you'll need to explicitly enable particular origins, methods, or headers, in order for browsers to be permitted to use them in a Cross-Domain context.

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware

routes = ...

middleware = [
    Middleware(CORSMiddleware, allow_origins=['*'])
]

app = Starlette(routes=routes, middleware=middleware)

The following arguments are supported:

  • allow_origins - A list of origins that should be permitted to make cross-origin requests. eg. ['https://example.org', 'https://www.example.org']. You can use ['*'] to allow any origin.
  • allow_origin_regex - A regex string to match against origins that should be permitted to make cross-origin requests. eg. 'https://.*\.example\.org'.
  • allow_methods - A list of HTTP methods that should be allowed for cross-origin requests. Defaults to ['GET']. You can use ['*'] to allow all standard methods.
  • allow_headers - A list of HTTP request headers that should be supported for cross-origin requests. Defaults to []. You can use ['*'] to allow all headers. The Accept, Accept-Language, Content-Language and Content-Type headers are always allowed for CORS requests.
  • allow_credentials - Indicate that cookies should be supported for cross-origin requests. Defaults to False.
  • expose_headers - Indicate any response headers that should be made accessible to the browser. Defaults to [].
  • max_age - Sets a maximum time in seconds for browsers to cache CORS responses. Defaults to 600.

The middleware responds to two particular types of HTTP request...

CORS preflight requests

These are any OPTIONS request with Origin and Access-Control-Request-Method headers. In this case the middleware will intercept the incoming request and respond with appropriate CORS headers, and either a 200 or 400 response for informational purposes.

Simple requests

Any request with an Origin header. In this case the middleware will pass the request through as normal, but will include appropriate CORS headers on the response.

SessionMiddleware

Adds signed cookie-based HTTP sessions. Session information is readable but not modifiable.

Access or modify the session data using the request.session dictionary interface.

The following arguments are supported:

  • secret_key - Should be a random string.
  • session_cookie - Defaults to "session".
  • max_age - Session expiry time in seconds. Defaults to 2 weeks. If set to None then the cookie will last as long as the browser session.
  • same_site - SameSite flag prevents the browser from sending session cookie along with cross-site requests. Defaults to 'lax'.
  • https_only - Indicate that Secure flag should be set (can be used with HTTPS only). Defaults to False.

HTTPSRedirectMiddleware

Enforces that all incoming requests must either be https or wss. Any incoming requests to http or ws will be redirected to the secure scheme instead.

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware

routes = ...

middleware = [
    Middleware(HTTPSRedirectMiddleware)
]

app = Starlette(routes=routes, middleware=middleware)

There are no configuration options for this middleware class.

TrustedHostMiddleware

Enforces that all incoming requests have a correctly set Host header, in order to guard against HTTP Host Header attacks.

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.trustedhost import TrustedHostMiddleware

routes = ...

middleware = [
    Middleware(TrustedHostMiddleware, allowed_hosts=['example.com', '*.example.com'])
]

app = Starlette(routes=routes, middleware=middleware)

The following arguments are supported:

  • allowed_hosts - A list of domain names that should be allowed as hostnames. Wildcard domains such as *.example.com are supported for matching subdomains. To allow any hostname either use allowed_hosts=["*"] or omit the middleware.

If an incoming request does not validate correctly then a 400 response will be sent.

GZipMiddleware

Handles GZip responses for any request that includes "gzip" in the Accept-Encoding header.

The middleware will handle both standard and streaming responses.

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.gzip import GZipMiddleware


routes = ...

middleware = [
    Middleware(GZipMiddleware, minimum_size=1000)
]

app = Starlette(routes=routes, middleware=middleware)

The following arguments are supported:

  • minimum_size - Do not GZip responses that are smaller than this minimum size in bytes. Defaults to 500.

The middleware won't GZip responses that already have a Content-Encoding set, to prevent them from being encoded twice.

BaseHTTPMiddleware

An abstract class that allows you to write ASGI middleware against a request/response interface.

Usage

To implement a middleware class using BaseHTTPMiddleware, you must override the async def dispatch(request, call_next) method.

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware


class CustomHeaderMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        response = await call_next(request)
        response.headers['Custom'] = 'Example'
        return response

routes = ...

middleware = [
    Middleware(CustomHeaderMiddleware)
]

app = Starlette(routes=routes, middleware=middleware)

If you want to provide configuration options to the middleware class you should override the __init__ method, ensuring that the first argument is app, and any remaining arguments are optional keyword arguments. Make sure to set the app attribute on the instance if you do this.

class CustomHeaderMiddleware(BaseHTTPMiddleware):
    def __init__(self, app, header_value='Example'):
        super().__init__(app)
        self.header_value = header_value

    async def dispatch(self, request, call_next):
        response = await call_next(request)
        response.headers['Custom'] = self.header_value
        return response


middleware = [
    Middleware(CustomHeaderMiddleware, header_value='Customized')
]

app = Starlette(routes=routes, middleware=middleware)

Middleware classes should not modify their state outside of the __init__ method. Instead you should keep any state local to the dispatch method, or pass it around explicitly, rather than mutating the middleware instance.

Limitations

Currently, the BaseHTTPMiddleware has some known limitations:

  • Using BaseHTTPMiddleware will prevent changes to contextlib.ContextVars from propagating upwards. That is, if you set a value for a ContextVar in your endpoint and try to read it from a middleware you will find that the value is not the same value you set in your endpoint (see this test for an example of this behavior).

To overcome these limitations, use pure ASGI middleware, as shown below.

Pure ASGI Middleware

The ASGI spec makes it possible to implement ASGI middleware using the ASGI interface directly, as a chain of ASGI applications that call into the next one. In fact, this is how middleware classes shipped with Starlette are implemented.

This lower-level approach provides greater control over behavior and enhanced interoperability across frameworks and servers. It also overcomes the limitations of BaseHTTPMiddleware.

Writing pure ASGI middleware

The most common way to create an ASGI middleware is with a class.

class ASGIMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        await self.app(scope, receive, send)

The middleware above is the most basic ASGI middleware. It receives a parent ASGI application as an argument for its constructor, and implements an async __call__ method which calls into that parent application.

Some implementations such as asgi-cors use an alternative style, using functions:

import functools

def asgi_middleware():
    def asgi_decorator(app):

        @functools.wraps(app)
        async def wrapped_app(scope, receive, send):
            await app(scope, receive, send)

        return wrapped_app

    return asgi_decorator

In any case, ASGI middleware must be callables that accept three arguments: scope, receive, and send.

  • scope is a dict holding information about the connection, where scope["type"] may be:
  • receive and send can be used to exchange ASGI event messages with the ASGI server — more on this below. The type and contents of these messages depend on the scope type. Learn more in the ASGI specification.

Using pure ASGI middleware

Pure ASGI middleware can be used like any other middleware:

from starlette.applications import Starlette
from starlette.middleware import Middleware

from .middleware import ASGIMiddleware

routes = ...

middleware = [
    Middleware(ASGIMiddleware),
]

app = Starlette(..., middleware=middleware)

See also Using middleware.

Type annotations

There are two ways of annotating a middleware: using Starlette itself or asgiref.

  • Using Starlette: for most common use cases.
from starlette.types import ASGIApp, Message, Scope, Receive, Send


class ASGIMiddleware:
    def __init__(self, app: ASGIApp) -> None:
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
        if scope["type"] != "http":
            return await self.app(scope, receive, send)

        async def send_wrapper(message: Message) -> None:
            # ... Do something
            await send(message)

        await self.app(scope, receive, send_wrapper)
  • Using asgiref: for more rigorous type hinting.
from asgiref.typing import ASGI3Application, ASGIReceiveCallable, ASGISendCallable, Scope
from asgiref.typing import ASGIReceiveEvent, ASGISendEvent


class ASGIMiddleware:
    def __init__(self, app: ASGI3Application) -> None:
        self.app = app

    async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        async def send_wrapper(message: ASGISendEvent) -> None:
            # ... Do something
            await send(message)

        return await self.app(scope, receive, send_wrapper)

Common patterns

Processing certain requests only

ASGI middleware can apply specific behavior according to the contents of scope.

For example, to only process HTTP requests, write this...

class ASGIMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        ...  # Do something here!

        await self.app(scope, receive, send)

Likewise, WebSocket-only middleware would guard on scope["type"] != "websocket".

The middleware may also act differently based on the request method, URL, headers, etc.

Reusing Starlette components

Starlette provides several data structures that accept the ASGI scope, receive and/or send arguments, allowing you to work at a higher level of abstraction. Such data structures include Request, Headers, QueryParams, URL, etc.

For example, you can instantiate a Request to more easily inspect an HTTP request:

from starlette.requests import Request

class ASGIMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope["type"] == "http":
            request = Request(scope)
            ... # Use `request.method`, `request.url`, `request.headers`, etc.

        await self.app(scope, receive, send)

You can also reuse responses, which are ASGI applications as well.

Sending eager responses

Inspecting the connection scope allows you to conditionally call into a different ASGI app. One use case might be sending a response without calling into the app.

As an example, this middleware uses a dictionary to perform permanent redirects based on the requested path. This could be used to implement ongoing support of legacy URLs in case you need to refactor route URL patterns.

from starlette.datastructures import URL
from starlette.responses import RedirectResponse

class RedirectsMiddleware:
    def __init__(self, app, path_mapping: dict):
        self.app = app
        self.path_mapping = path_mapping

    async def __call__(self, scope, receive, send):
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        url = URL(scope=scope)

        if url.path in self.path_mapping:
            url = url.replace(path=self.path_mapping[url.path])
            response = RedirectResponse(url, status_code=301)
            await response(scope, receive, send)
            return

        await self.app(scope, receive, send)

Example usage would look like this:

from starlette.applications import Starlette
from starlette.middleware import Middleware

routes = ...

redirections = {
    "/v1/resource/": "/v2/resource/",
    # ...
}

middleware = [
    Middleware(RedirectsMiddleware, path_mapping=redirections),
]

app = Starlette(routes=routes, middleware=middleware)

Inspecting or modifying the request

Request information can be accessed or changed by manipulating the scope. For a full example of this pattern, see Uvicorn's ProxyHeadersMiddleware which inspects and tweaks the scope when serving behind a frontend proxy.

Besides, wrapping the receive ASGI callable allows you to access or modify the HTTP request body by manipulating http.request ASGI event messages.

As an example, this middleware computes and logs the size of the incoming request body...

class LoggedRequestBodySizeMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        body_size = 0

        async def receive_logging_request_body_size():
            nonlocal body_size

            message = await receive()
            assert message["type"] == "http.request"

            body_size += len(message.get("body", b""))

            if not message.get("more_body", False):
                print(f"Size of request body was: {body_size} bytes")

            return message

        await self.app(scope, receive_logging_request_body_size, send)

Likewise, WebSocket middleware may manipulate websocket.receive ASGI event messages to inspect or alter incoming WebSocket data.

For an example that changes the HTTP request body, see msgpack-asgi.

Inspecting or modifying the response

Wrapping the send ASGI callable allows you to inspect or modify the HTTP response sent by the underlying application. To do so, react to http.response.start or http.response.body ASGI event messages.

As an example, this middleware adds some fixed extra response headers:

from starlette.datastructures import MutableHeaders

class ExtraResponseHeadersMiddleware:
    def __init__(self, app, headers):
        self.app = app
        self.headers = headers

    async def __call__(self, scope, receive, send):
        if scope["type"] != "http":
            return await self.app(scope, receive, send)

        async def send_with_extra_headers(message):
            if message["type"] == "http.response.start":
                headers = MutableHeaders(scope=message)
                for key, value in self.headers:
                    headers.append(key, value)

            await send(message)

        await self.app(scope, receive, send_with_extra_headers)

See also asgi-logger for an example that inspects the HTTP response and logs a configurable HTTP access log line.

Likewise, WebSocket middleware may manipulate websocket.send ASGI event messages to inspect or alter outgoing WebSocket data.

Note that if you change the response body, you will need to update the response Content-Length header to match the new response body length. See brotli-asgi for a complete example.

Passing information to endpoints

If you need to share information with the underlying app or endpoints, you may store it into the scope dictionary. Note that this is a convention -- for example, Starlette uses this to share routing information with endpoints -- but it is not part of the ASGI specification. If you do so, be sure to avoid conflicts by using keys that have low chances of being used by other middleware or applications.

For example, when including the middleware below, endpoints would be able to access request.scope["asgi_transaction_id"].

import uuid

class TransactionIDMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        scope["asgi_transaction_id"] = uuid.uuid4()
        await self.app(scope, receive, send)

Cleanup and error handling

You can wrap the application in a try/except/finally block or a context manager to perform cleanup operations or do error handling.

For example, the following middleware might collect metrics and process application exceptions...

import time

class MonitoringMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        start = time.time()
        try:
            await self.app(scope, receive, send)
        except Exception as exc:
            ...  # Process the exception
            raise
        finally:
            end = time.time()
            elapsed = end - start
            ...  # Submit `elapsed` as a metric to a monitoring backend

See also timing-asgi for a full example of this pattern.

Gotchas

ASGI middleware should be stateless

Because ASGI is designed to handle concurrent requests, any connection-specific state should be scoped to the __call__ implementation. Not doing so would typically lead to conflicting variable reads/writes across requests, and most likely bugs.

As an example, this would conditionally replace the response body, if an X-Mock header is present in the response...

from starlette.datastructures import Headers

class MockResponseBodyMiddleware:
    def __init__(self, app, content):
        self.app = app
        self.content = content

    async def __call__(self, scope, receive, send):
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        # A flag that we will turn `True` if the HTTP response
        # has the 'X-Mock' header.
        # ✅: Scoped to this function.
        should_mock = False

        async def maybe_send_with_mock_content(message):
            nonlocal should_mock

            if message["type"] == "http.response.start":
                headers = Headers(raw=message["headers"])
                should_mock = headers.get("X-Mock") == "1"
                await send(message)

            elif message["type"] == "http.response.body":
                if should_mock:
                    message = {"type": "http.response.body", "body": self.content}
                await send(message)

        await self.app(scope, receive, maybe_send_with_mock_content)
from starlette.datastructures import Headers

class MockResponseBodyMiddleware:
    def __init__(self, app, content):
        self.app = app
        self.content = content
        # ❌: This variable would be read and written across requests!
        self.should_mock = False

    async def __call__(self, scope, receive, send):
        if scope["type"] != "http":
            await self.app(scope, receive, send)
            return

        async def maybe_send_with_mock_content(message):
            if message["type"] == "http.response.start":
                headers = Headers(raw=message["headers"])
                self.should_mock = headers.get("X-Mock") == "1"
                await send(message)

            elif message["type"] == "http.response.body":
                if self.should_mock:
                    message = {"type": "http.response.body", "body": self.content}
                await send(message)

        await self.app(scope, receive, maybe_send_with_mock_content)

See also GZipMiddleware for a full example implementation that navigates this potential gotcha.

Further reading

This documentation should be enough to have a good basis on how to create an ASGI middleware.

Nonetheless, there are great articles about the subject:

Using middleware in other frameworks

To wrap ASGI middleware around other ASGI applications, you should use the more general pattern of wrapping the application instance:

app = TrustedHostMiddleware(app, allowed_hosts=['example.com'])

You can do this with a Starlette application instance too, but it is preferable to use the middleware=<List of Middleware instances> style, as it will:

  • Ensure that everything remains wrapped in a single outermost ServerErrorMiddleware.
  • Preserves the top-level app instance.

Applying middleware to Mounts

Middleware can also be added to Mount, which allows you to apply middleware to a single route, a group of routes or any mounted ASGI application:

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.gzip import GZipMiddleware
from starlette.routing import Mount, Route


routes = [
    Mount(
        "/",
        routes=[
            Route(
                "/example",
                endpoint=...,
            )
        ],
        middleware=[Middleware(GZipMiddleware)]
    )
]

app = Starlette(routes=routes)

Note that middleware used in this way is not wrapped in exception handling middleware like the middleware applied to the Starlette application is. This is often not a problem because it only applies to middleware that inspect or modify the Response, and even then you probably don't want to apply this logic to error responses. If you do want to apply the middleware logic to error responses only on some routes you have a couple of options:

  • Add an ExceptionMiddleware onto the Mount
  • Add a try/except block to your middleware and return an error response from there
  • Split up marking and processing into two middlewares, one that gets put on Mount which marks the response as needing processing (for example by setting scope["log-response"] = True) and another applied to the Starlette application that does the heavy lifting.

Third party middleware

asgi-auth-github

This middleware adds authentication to any ASGI application, requiring users to sign in using their GitHub account (via OAuth). Access can be restricted to specific users or to members of specific GitHub organizations or teams.

asgi-csrf

Middleware for protecting against CSRF attacks. This middleware implements the Double Submit Cookie pattern, where a cookie is set, then it is compared to a csrftoken hidden form field or an x-csrftoken HTTP header.

AuthlibMiddleware

A drop-in replacement for Starlette session middleware, using authlib's jwt module.

BugsnagMiddleware

A middleware class for logging exceptions to Bugsnag.

CSRFMiddleware

Middleware for protecting against CSRF attacks. This middleware implements the Double Submit Cookie pattern, where a cookie is set, then it is compared to an x-csrftoken HTTP header.

EarlyDataMiddleware

Middleware and decorator for detecting and denying TLSv1.3 early data requests.

PrometheusMiddleware

A middleware class for capturing Prometheus metrics related to requests and responses, including in progress requests, timing...

ProxyHeadersMiddleware

Uvicorn includes a middleware class for determining the client IP address, when proxy servers are being used, based on the X-Forwarded-Proto and X-Forwarded-For headers. For more complex proxy configurations, you might want to adapt this middleware.

RateLimitMiddleware

A rate limit middleware. Regular expression matches url; flexible rules; highly customizable. Very easy to use.

RequestIdMiddleware

A middleware class for reading/generating request IDs and attaching them to application logs.

RollbarMiddleware

A middleware class for logging exceptions, errors, and log messages to Rollbar.

SentryMiddleware

A middleware class for logging exceptions to Sentry.

StarletteOpentracing

A middleware class that emits tracing info to OpenTracing.io compatible tracers and can be used to profile and monitor distributed applications.

TimingMiddleware

A middleware class to emit timing information (cpu and wall time) for each request which passes through it. Includes examples for how to emit these timings as statsd metrics.

WSGIMiddleware

A middleware class in charge of converting a WSGI application into an ASGI one.