Skip to content

    Chapter 14, Intermediate

    Middleware

    The middleware is a layer that **every** request passes through, both on the way in and on the way out. It is the right place for what applies to the entire application: measuring time, identifying the request, compressing responses.

    How it works

    A middleware receives the request before before it reaches the route and the response after the route responds. What comes before the await call_next(request) runs on the way in, and what comes after runs on the way out. The call_next passes the request along:

    intermediario/cap14_middleware.pylinhas 10 a 29
    import time
    import uuid
    
    from fastapi import FastAPI, Request
    from fastapi.testclient import TestClient
    
    app = FastAPI()
    
    
    @app.middleware("http")
    async def medir_tempo(request: Request, call_next):
        inicio = time.perf_counter()
        resposta = await call_next(request)
        resposta.headers["X-Process-Time"] = f"{time.perf_counter() - inicio:.6f}"
        return resposta
    
    
    @app.get("/")
    def home():
        return {"mensagem": "ok"}
    

    The middleware measures the time taken from when the request enters until the response goes out, and returns it in a header. Any route in the application gains this without changing a line. The middleware is async def because it runs in the event loop of all requests.

    Identify each request

    A request identifier allows you to link a customer complaint to the server logs. The middleware creates one (or reuses what came from a proxy), stores it in request.state for the route to use, and returns it in the header:

    intermediario/cap14_middleware.pylinhas 34 a 53
    @app.middleware("http")
    async def id_da_requisicao(request: Request, call_next):
        request.state.request_id = request.headers.get("x-request-id") or str(uuid.uuid4())
        resposta = await call_next(request)
        resposta.headers["X-Request-ID"] = request.state.request_id
        return resposta
    
    
    @app.get("/quem-sou")
    def quem_sou(request: Request):
        return {"request_id": request.state.request_id}
    
    
    cliente = TestClient(app)
    resposta = cliente.get("/")
    print(resposta.status_code, float(resposta.headers["x-process-time"]) >= 0)
    
    sem_id = cliente.get("/quem-sou")
    print(len(sem_id.headers["x-request-id"]), sem_id.json()["request_id"] == sem_id.headers["x-request-id"])
    print(cliente.get("/quem-sou", headers={"x-request-id": "abc-123"}).json())
    
    Output
    200 True
    36 True
    {'request_id': 'abc-123'}
    

    The two middlewares work together: the route / received the time header, and the /quem-sou read the identifier that the other middleware left in request.state.

    Middlewares need to be ready before the first request

    If you try to add a middleware after the application has already responded to a request, Starlette refuses with RuntimeError: Cannot add middleware after an application has started. In a real program, this does not appear because everything is declared when the module is imported. Therefore, in this chapter, the TestClient is only created after the middlewares are declared.

    The order of middlewares

    When there are several, the the last registered is left out: he is the first to see the request and the last to see the response. You can see the drawing:

    intermediario/cap14_middleware.pylinhas 58 a 85
    ordem = []
    app_ordem = FastAPI()
    
    
    @app_ordem.middleware("http")
    async def primeiro(request: Request, call_next):
        ordem.append("primeiro: ida")
        resposta = await call_next(request)
        ordem.append("primeiro: volta")
        return resposta
    
    
    @app_ordem.middleware("http")
    async def segundo(request: Request, call_next):
        ordem.append("segundo: ida")
        resposta = await call_next(request)
        ordem.append("segundo: volta")
        return resposta
    
    
    @app_ordem.get("/")
    def raiz():
        ordem.append("rota")
        return {}
    
    
    TestClient(app_ordem).get("/")
    print(ordem)
    
    Output
    ['segundo: ida', 'primeiro: ida', 'rota', 'primeiro: volta', 'segundo: volta']
    

    Middlewares that come ready-made

    FastAPI includes the most common ones. The GZipMiddleware, for example, compresses responses larger than a minimum size when the client says it accepts:

    intermediario/cap14_middleware.pylinhas 90 a 108
    from fastapi.middleware.gzip import GZipMiddleware
    
    app_gzip = FastAPI()
    app_gzip.add_middleware(GZipMiddleware, minimum_size=500)
    
    
    @app_gzip.get("/grande")
    def grande():
        return {"texto": "repetição " * 500}
    
    
    @app_gzip.get("/pequena")
    def pequena():
        return {"texto": "oi"}
    
    
    gz = TestClient(app_gzip)
    print(gz.get("/grande", headers={"accept-encoding": "gzip"}).headers.get("content-encoding"))
    print(gz.get("/pequena", headers={"accept-encoding": "gzip"}).headers.get("content-encoding"))
    
    Output
    gzip
    None
    

    Others you will use: the CORSMiddleware (chapter 22) and the TrustedHostMiddleware, which rejects requests with an unexpected header Host .

    Middleware, dependency, and handler

    MiddlewareDependencyException handler
    ScopeEvery requestThe routes that request itWhen a specific exception occurs
    Sees the responseYesNoCreates the error response
    Good forTime, request ID, compression, headersAuthentication, database, route validationsUnique error format
    Receives parameters from the routeNoYes, with validationNo

    Be careful with what the middleware does

    The middleware runs on all the requests, including those of /docs. Slow or blocking work in it delays the entire application. And the @app.middleware("http") is built on top of BaseHTTPMiddleware, which has limitations with streaming responses. For the critical path of heavily loaded applications, there is pure ASGI middleware, more complex, but without this cost.

    Exercise 1

    A version header

    Create a middleware that adds X-App-Version: 1.0 to all the responses, and two routes to check.