diff --git a/docs/en/docs/advanced/middleware.md b/docs/en/docs/advanced/middleware.md index 1d40b1c8f..792f8cfc7 100644 --- a/docs/en/docs/advanced/middleware.md +++ b/docs/en/docs/advanced/middleware.md @@ -84,6 +84,20 @@ The following arguments are supported: * `minimum_size` - Do not GZip responses that are smaller than this minimum size in bytes. Defaults to `500`. * `compresslevel` - Used during GZip compression. It is an integer ranging from 1 to 9. Defaults to `9`. Lower value results in faster compression but larger file sizes, while higher value results in slower compression but smaller file sizes. +## Router and Route-Level Middleware Example + +**FastAPI supports adding scoped middleware per route and router. Middleware execution order:** + +- **App‑level middleware** runs on *every* request as soon as it enters your application, before any router or route is matched. +- **Router‑level middleware** runs next, wrapping all requests to routes included on that router. +- **Route‑level middleware** runs last, just around the specific path operation. + +This gives better control over where and when logic executes. + +The example below shows middleware applied at each scope. Notice how the inner route’s middleware is able to match path params ;) + +{* ../../docs_src/advanced_middleware/tutorial004.py *} + ## Other middlewares There are many other ASGI middlewares. diff --git a/docs_src/advanced_middleware/tutorial004.py b/docs_src/advanced_middleware/tutorial004.py new file mode 100644 index 000000000..bbe1e3572 --- /dev/null +++ b/docs_src/advanced_middleware/tutorial004.py @@ -0,0 +1,61 @@ +from fastapi import APIRouter, FastAPI, Request +from starlette.middleware import Middleware +from starlette.middleware.base import BaseHTTPMiddleware + +app = FastAPI() + + +@app.middleware("http") +async def app_middleware(request: Request, call_next): + print("App before") + response = await call_next(request) + print("App after") + return response + + +async def outer_middleware(request: Request, call_next): + print("Outer before") + response = await call_next(request) + print("Outer after") + return response + + +outer = APIRouter( + prefix="/outer", + middleware=[Middleware(BaseHTTPMiddleware, dispatch=outer_middleware)], +) + + +async def name_middleware(request: Request, call_next): + print(f"Hi {request.path_params.get('name')}!") + response = await call_next(request) + print(f"Bye {request.path_params.get('name')}!") + return response + + +inner = APIRouter(prefix="/inner") + + +@inner.get( + "/{name}", + middleware=[Middleware(BaseHTTPMiddleware, dispatch=name_middleware)], +) +async def hello(name: str): + print("Handler") + return {"message": f"Hello {name} from inner!"} + + +@outer.get("/") +async def outer_hello(): + print("Handler") + return {"message": "Hello from outer!"} + + +@app.get("/") +async def app_hello(): + print("Handler") + return {"message": "Hello from app!"} + + +outer.include_router(inner) +app.include_router(outer) diff --git a/fastapi/applications.py b/fastapi/applications.py index 05c7bd2be..aef40928d 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1084,6 +1084,7 @@ class FastAPI(Starlette): generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( generate_unique_id ), + middleware: Optional[Sequence[Middleware]] = None, ) -> None: self.router.add_api_route( path, @@ -1110,6 +1111,7 @@ class FastAPI(Starlette): name=name, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def api_route( @@ -1140,6 +1142,7 @@ class FastAPI(Starlette): generate_unique_id_function: Callable[[routing.APIRoute], str] = Default( generate_unique_id ), + middleware: Optional[Sequence[Middleware]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: def decorator(func: DecoratedCallable) -> DecoratedCallable: self.router.add_api_route( @@ -1167,6 +1170,7 @@ class FastAPI(Starlette): name=name, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) return func @@ -1788,6 +1792,21 @@ class FastAPI(Starlette): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP GET operation. @@ -1828,6 +1847,7 @@ class FastAPI(Starlette): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def put( @@ -2161,6 +2181,21 @@ class FastAPI(Starlette): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP PUT operation. @@ -2206,6 +2241,7 @@ class FastAPI(Starlette): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def post( @@ -2539,6 +2575,21 @@ class FastAPI(Starlette): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP POST operation. @@ -2584,6 +2635,7 @@ class FastAPI(Starlette): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def delete( @@ -2917,6 +2969,21 @@ class FastAPI(Starlette): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP DELETE operation. @@ -2957,6 +3024,7 @@ class FastAPI(Starlette): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def options( @@ -3290,6 +3358,21 @@ class FastAPI(Starlette): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP OPTIONS operation. @@ -3330,6 +3413,7 @@ class FastAPI(Starlette): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def head( @@ -3663,6 +3747,21 @@ class FastAPI(Starlette): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP HEAD operation. @@ -3703,6 +3802,7 @@ class FastAPI(Starlette): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def patch( @@ -4036,6 +4136,21 @@ class FastAPI(Starlette): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP PATCH operation. @@ -4081,6 +4196,7 @@ class FastAPI(Starlette): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def trace( @@ -4414,6 +4530,21 @@ class FastAPI(Starlette): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP TRACE operation. @@ -4454,6 +4585,7 @@ class FastAPI(Starlette): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def websocket_route( diff --git a/fastapi/routing.py b/fastapi/routing.py index 54c75a027..d4b758f0c 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -61,6 +61,7 @@ from pydantic import BaseModel from starlette import routing from starlette.concurrency import run_in_threadpool from starlette.exceptions import HTTPException +from starlette.middleware import Middleware from starlette.requests import Request from starlette.responses import JSONResponse, Response from starlette.routing import ( @@ -460,6 +461,7 @@ class APIRoute(routing.Route): generate_unique_id_function: Union[ Callable[["APIRoute"], str], DefaultPlaceholder ] = Default(generate_unique_id), + middleware: Optional[Sequence[Middleware]] = None, ) -> None: self.path = path self.endpoint = endpoint @@ -567,8 +569,13 @@ class APIRoute(routing.Route): name=self.unique_id, embed_body_fields=self._embed_body_fields, ) + self.middleware = middleware self.app = request_response(self.get_route_handler()) + if middleware is not None: + for cls, args, kwargs in reversed(middleware): + self.app = cls(self.app, *args, **kwargs) + def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]: return get_request_handler( dependant=self.dependant, @@ -834,6 +841,21 @@ class APIRouter(routing.Router): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this router. + + Router-level middleware is executed after application-level middleware. + When multiple routers declare middleware, the outermost (furthest) router's middleware runs first. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> None: super().__init__( routes=routes, @@ -842,6 +864,7 @@ class APIRouter(routing.Router): on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan, + middleware=middleware, ) if prefix: assert prefix.startswith("/"), "A path prefix must start with '/'" @@ -859,6 +882,7 @@ class APIRouter(routing.Router): self.route_class = route_class self.default_response_class = default_response_class self.generate_unique_id_function = generate_unique_id_function + self.middleware = middleware def route( self, @@ -912,6 +936,7 @@ class APIRouter(routing.Router): generate_unique_id_function: Union[ Callable[[APIRoute], str], DefaultPlaceholder ] = Default(generate_unique_id), + middleware: Optional[Sequence[Middleware]] = None, ) -> None: route_class = route_class_override or self.route_class responses = responses or {} @@ -931,6 +956,9 @@ class APIRouter(routing.Router): current_generate_unique_id = get_value_or_default( generate_unique_id_function, self.generate_unique_id_function ) + if middleware and self.middleware: + middleware = list(self.middleware) + list(middleware) + route = route_class( self.prefix + path, endpoint=endpoint, @@ -958,6 +986,7 @@ class APIRouter(routing.Router): callbacks=current_callbacks, openapi_extra=openapi_extra, generate_unique_id_function=current_generate_unique_id, + middleware=middleware or self.middleware, ) self.routes.append(route) @@ -990,6 +1019,7 @@ class APIRouter(routing.Router): generate_unique_id_function: Callable[[APIRoute], str] = Default( generate_unique_id ), + middleware: Optional[Sequence[Middleware]] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: def decorator(func: DecoratedCallable) -> DecoratedCallable: self.add_api_route( @@ -1018,6 +1048,7 @@ class APIRouter(routing.Router): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) return func @@ -1329,6 +1360,7 @@ class APIRouter(routing.Router): callbacks=current_callbacks, openapi_extra=route.openapi_extra, generate_unique_id_function=current_generate_unique_id, + middleware=route.middleware, ) elif isinstance(route, routing.Route): methods = list(route.methods or []) @@ -1695,6 +1727,21 @@ class APIRouter(routing.Router): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP GET operation. @@ -1739,6 +1786,7 @@ class APIRouter(routing.Router): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def put( @@ -2072,6 +2120,21 @@ class APIRouter(routing.Router): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP PUT operation. @@ -2121,6 +2184,7 @@ class APIRouter(routing.Router): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def post( @@ -2454,6 +2518,21 @@ class APIRouter(routing.Router): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP POST operation. @@ -2503,6 +2582,7 @@ class APIRouter(routing.Router): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def delete( @@ -2836,6 +2916,21 @@ class APIRouter(routing.Router): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP DELETE operation. @@ -2880,6 +2975,7 @@ class APIRouter(routing.Router): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def options( @@ -3213,6 +3309,21 @@ class APIRouter(routing.Router): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP OPTIONS operation. @@ -3257,6 +3368,7 @@ class APIRouter(routing.Router): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def head( @@ -3590,6 +3702,21 @@ class APIRouter(routing.Router): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP HEAD operation. @@ -3639,6 +3766,7 @@ class APIRouter(routing.Router): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def patch( @@ -3972,6 +4100,21 @@ class APIRouter(routing.Router): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP PATCH operation. @@ -4021,6 +4164,7 @@ class APIRouter(routing.Router): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) def trace( @@ -4354,6 +4498,21 @@ class APIRouter(routing.Router): """ ), ] = Default(generate_unique_id), + middleware: Annotated[ + Optional[Sequence[Middleware]], + Doc( + """ + List of middleware to apply to all requests handled by this route. + + Route-level middleware is executed after application-level and router-level middleware. + Any middleware declared on the router will be called before this route's middleware. + Middleware are applied in reverse order: the last middleware in this list is the first to be called. + + Read more about it in the + [FastAPI docs for Middleware](https://fastapi.tiangolo.com/advanced/middleware/) + """ + ), + ] = None, ) -> Callable[[DecoratedCallable], DecoratedCallable]: """ Add a *path operation* using an HTTP TRACE operation. @@ -4403,6 +4562,7 @@ class APIRouter(routing.Router): callbacks=callbacks, openapi_extra=openapi_extra, generate_unique_id_function=generate_unique_id_function, + middleware=middleware, ) @deprecated( diff --git a/tests/test_tutorial/test_advanced_middleware/test_tutorial004.py b/tests/test_tutorial/test_advanced_middleware/test_tutorial004.py new file mode 100644 index 000000000..c182c59d1 --- /dev/null +++ b/tests/test_tutorial/test_advanced_middleware/test_tutorial004.py @@ -0,0 +1,55 @@ +from fastapi.testclient import TestClient + +from docs_src.advanced_middleware.tutorial004 import app + +client = TestClient(app) + + +def test_app_middleware_called_once(capsys): + r = client.get("/") + assert r.status_code == 200 + + captured = capsys.readouterr().out + assert captured.count("App before") == 1 + assert captured.count("Outer before") == 0 + assert captured.count("Handler") == 1 + assert captured.count("Outer after") == 0 + assert captured.count("App after") == 1 + + +def test_outer_middleware_called_once(capsys): + r = client.get("/outer/") + assert r.status_code == 200 + + captured = capsys.readouterr().out + assert captured.count("App before") == 1 + assert captured.count("Outer before") == 1 + assert captured.count("Handler") == 1 + assert captured.count("Outer after") == 1 + assert captured.count("App after") == 1 + + +def test_name_middleware_called_once(capsys): + name = "you" + r = client.get(f"/outer/inner/{name}") + assert r.status_code == 200 + assert r.json() == {"message": f"Hello {name} from inner!"} + + captured = capsys.readouterr().out + seq = [ + "App before", + "Outer before", + f"Hi {name}!", + "Handler", + f"Bye {name}!", + "Outer after", + "App after", + ] + for msg in seq: + assert captured.count(msg) == 1 + + idx = 0 + for msg in seq: + next_idx = captured.find(msg, idx) + assert next_idx >= idx + idx = next_idx