How to use different middleware for different routes/path #7691
Replies: 29 comments 6 replies
-
I haven't use this way, but you can have a try. |
Beta Was this translation helpful? Give feedback.
-
Thank you @Dustyposa this looks exactly like what I was looking for. Here is a example implementation in case someone else stumble upon the same problem # app1.py
from fastapi import FastAPI
app1 = FastAPI(openapi_prefix="/app1")
@app1.get("path1")
async def path1():
return { "message": "app1"} # app2.py
from fastapi import FastAPI
app2 = FastAPI(openapi_prefix="/app2")
@app1.get("path2")
async def path2():
return { "message": "app2"} # main.py
from fastapi import FastAPI
from app1 import app1
from app2 import app2
app1.add_middleware(
CORSMiddleware,
allow_origins=['myfrontend.com'],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# here we can add middlewares that are only for app2 and not executed on paths of app1
# the CORS policy is different for app2. (allows '*' instead of 'myfrontend.com')
app2.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app = FastAPI()
app.mount("/app1", app1) # if does not work try: app.mount(app1, "/app1")
app.mount("/app2", app2) # if does not work try: app.mount(app2, "/app2")
@app.get("/")
async def root():
return {"message": "alive"} |
Beta Was this translation helpful? Give feedback.
-
Thanks for your help here @Dustyposa 🙇♂️ 🍰 And thanks for reporting back and closing the issue @philippegirard 🚀 |
Beta Was this translation helpful? Give feedback.
-
This is a fine approach for some use cases. But having route based middleware is still a requirement for many projects I work on. I understand starlette itself cannot yet do this, so perhaps this is the wrong github page to bring it up. I will see what Tom Cristie thinks about adding route based middleware to starlette. Then perhaps fastapi can impliment it in the route decorators |
Beta Was this translation helpful? Give feedback.
-
I'm facing a quite similar issue : I want to add custom 'Content-Security-Policy' header the easiest way to do so is to add a middleware : @Api.middleware("http")
However this CSP block the Swagger UI and thus it would be great to only apply the middleware to a subset of routes. nb1 : For those who are wondering, i removed the middleware and i had to add a response headers to all routes (ie functions) except for the swaggers (not very clean) |
Beta Was this translation helpful? Give feedback.
-
+1 for router and/or route level middleware |
Beta Was this translation helpful? Give feedback.
-
Thanks @philippegirard. It's helped me, but in your example: app.mount(app1, "/app1")
app.mount(app2, "/app2") Not working. Checking the documentation, I realized that the correct way is swapping parameters: app.mount("/app1", app1)
app.mount("/app2", app2) |
Beta Was this translation helpful? Give feedback.
-
An interesting alternative is to use customization of the I wonder if we could wrap an existing middleware into an router = APIRouter(route_class=SomeWrapper(middleware=[Middleware1, Middleware2])) |
Beta Was this translation helpful? Give feedback.
-
And... here's a super quick & dirty implementation + 1 test: from typing import List, Optional, Type
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import APIRouter, FastAPI, Response
from fastapi.routing import APIRoute
from starlette.middleware import Middleware
from starlette.types import ASGIApp
# define a vanilla middleware
class CustomHeaderMiddleware(BaseHTTPMiddleware):
def __init__(self, app: ASGIApp, header_value: str):
super().__init__(app)
self.header_value = header_value
async def dispatch(self, request, call_next):
response: Response = await call_next(request)
response.headers['X-Custom'] = self.header_value
return response
# bind the middleware to an APIRoute subclass
def MiddlewareWrapper(middleware: Optional[List[Middleware]] = None) -> Type[APIRoute]:
class CustomAPIRoute(APIRoute):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
app = self.app
for cls, options in reversed(middleware or []):
app = cls(app, **options)
self.app = app
return CustomAPIRoute
app = FastAPI()
router = APIRouter(route_class=MiddlewareWrapper(middleware=[Middleware(CustomHeaderMiddleware, header_value="test")]))
@router.get("/")
async def root():
...
app.include_router(router)
from fastapi.testclient import TestClient
client = TestClient(app)
assert client.get("/").headers["X-Custom"] == "test" Obviously some things will be broken: these middleware can't modify the path (this would silently fail) and there's probably other stuff that won't work 🤷 , but for a lot of things this is probably fine. As far as I can tell, this isn't accessing any private attributes or doing other breakable stuff. I'm sure the class closure thing can be cleaned up in some way. This would be even easier if we were able to edit parameters on |
Beta Was this translation helpful? Give feedback.
-
I opened encode/starlette#1286 to see if we can upstream #1174 (comment) into Starlette (and then get it for free in FastAPI). The implementation there is 4 LOC. |
Beta Was this translation helpful? Give feedback.
-
encode/starlette#1286 is implemented at the app = FastAPI()
@app.get("/", middleware=[...]) As well as the original request: router = APIRouter(middleware=[...])
@router.get("/") Even combining both (although we'd have to determine the order in which the sequence of middlewares is combined). |
Beta Was this translation helpful? Give feedback.
-
@philippegirard which middleware would execute first by default? Can we control the order of execution of the middlewares? |
Beta Was this translation helpful? Give feedback.
-
@abhaynayak24 I am not sure. In this example there's is only one middleware per route. Therefore, there exists no routes which executes two middleware in the same request this example. Experiment to answer your question: Add two middleware to the same route with two different logs and check in the console which one executes first. You can report back on this issue the result of your experiment. Thank :) ! |
Beta Was this translation helpful? Give feedback.
-
It is not the cleanest thing in the world but it occurred to me to create a list with the paths that I want to go through the middleware, and within the middleware a routing condition
In this way, if your middleware does validations, they do not block the other routes such as the api / docs for example |
Beta Was this translation helpful? Give feedback.
-
Hi, the document has moved to a new link . |
Beta Was this translation helpful? Give feedback.
-
I feel like having to use sub apps is not the optimal choice, I would like to be able to set in a route or path level which middlewares and their order should be running there. Is this still not possible in fastapi? |
Beta Was this translation helpful? Give feedback.
-
It won't be possible until encode/starlette#1286 gets merged and FastAPI updates it's Starlette version. Or just copy the implementation in encode/starlette#1286 to a custom subclass of FastAPI's APIRouter if you need it today. |
Beta Was this translation helpful? Give feedback.
-
Any news here? I have a need for route specific middleware. |
Beta Was this translation helpful? Give feedback.
-
Also would love to see route level middleware. EDIT: Current work on this is pending, here: Starlette PR#1464 |
Beta Was this translation helpful? Give feedback.
-
The best way to push this forward would be to express your support in encode/starlette#1464 via a 👍. Maybe also consider donating or having your employer donate to https://github.com/encode/ so that maintainers can spend more time working on Starlette (to the benefit of all downstream projects, including FastAPI) and less time worrying about being a person in a world run by purchasing power. |
Beta Was this translation helpful? Give feedback.
-
A bit late to the party but, Recently I wanted to have an authentication middleware on some of my FastAPI routers, just like you'd do in the Express.JS project. Here's my StackOverflow answer to a similar question: https://stackoverflow.com/a/73233603/10123687 As for the CORS use case, it won't be possible to use the middlewares provided by Starlette, but you can easily write your own and add/modify the response headers. something like this should work: async def CORS(request: Request, response: Response):
response.headers['Access-Control-Allow-Origin'] = 'http://localhost:8000'
response.headers['Access-Control-Allow-Methods'] = 'OPTIONS,GET,POST'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
router = APIRouter(dependencies=[
Depends(CORS)
])
# ... Your routes as usual
@router.get('/xyz')
# ... |
Beta Was this translation helpful? Give feedback.
-
@adriangb it seems like per-route middlewares would be unblocked as the latest pulls in starlette 0.25 which has your changes. Would a PR adding it to FastAPI be likely to get merged? Having separate routers sort of works but for some use cases where you need some route specific configuration in the middleware it gets very clunky. |
Beta Was this translation helpful? Give feedback.
-
I think that yes a PR to FastAPI would be accepted |
Beta Was this translation helpful? Give feedback.
-
Hello, everyone! Let me describe the scenarios and problems
where the api_router is an APIRouter object which includes all endpoints from some dedicated file (something like resource.py) then is it worth it a functionality like this one:
I came up with something the below code (really fast coded, so need some refactoring). I would be really happy to hear what do you think - is it something that might be helpful? Do you see security concerns?
|
Beta Was this translation helpful? Give feedback.
-
Hi, Not sure if this can help, but instead of overriding the functionality of the main CORSMiddleware class, I wrote a quick class that manages different CORS settings for different paths, so that multiple CORS settings can be set for the same API. import re
from typing import List, Union, Sequence, Optional
from pydantic import BaseModel, Field
from starlette.middleware.cors import CORSMiddleware
from starlette.types import ASGIApp, Receive, Scope, Send
class CORSSettings(BaseModel):
allow_origins: Sequence[str] = ()
allow_methods: Sequence[str] = ("GET",)
allow_headers: Sequence[str] = ()
allow_credentials: bool = False
allow_origin_regex: Optional[str] = None
expose_headers: Sequence[str] = ()
max_age: int = 600
path_regex: Union[List[str], Sequence[str]] = Field(default_factory=tuple, exclude=True)
class PathsCORSMiddleware:
"""Specify different CORS settings for different paths"""
def __init__(self, app: ASGIApp, paths_cors: List[CORSSettings], default_cors: CORSSettings) -> None:
# Path regexes mapping
patterns = [(idx, re.compile(path)) for idx, settings in enumerate(paths_cors) for path in settings.path_regex]
self.indexes, self.patterns = zip(*patterns)
# CORS settings mapping
self.paths_cors_middlewares = {idx: CORSMiddleware(app=app, **settings.model_dump())
for idx, settings in enumerate(paths_cors)}
self.default_cors = CORSMiddleware(app=app, **default_cors.model_dump())
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
# Identify relevant CORS middleware
path = scope["path"]
matches = [pattern.fullmatch(path) is not None for pattern in self.patterns]
if any(matches):
if sum(matches) == 1:
# Identify index of matching path & call middleware
idx = matches.index(True)
cors_middleware = self.paths_cors_middlewares[idx]
await cors_middleware(scope=scope, receive=receive, send=send)
else:
matching_patterns = [pattern.pattern for match, pattern in zip(matches, self.patterns) if match]
raise ValueError(f"Multiple CORS settings match the path. "
f"Matching patterns: {','.join(matching_patterns)}")
else:
# Call default CORS middleware
await self.default_cors(scope=scope, receive=receive, send=send) The actual configuration can then be done similar to this: PATHS_CORS = [
CORSSettings(
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
path_regex=["/somepath.*"]
)
]
DEFAULT_CORS = CORSSettings(
allow_origins=["your.restricted.domain"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
) and the CORS middleware can be added to the FastAPI application as follows: app = FastAPI()
app.add_middleware(
PathsCORSMiddleware,
paths_cors=PATHS_CORS,
default_cors=DEFAULT_CORS
) When a request will be coming in, the PathsCORSMIddleware is called up and the relevant CORSMiddleware is then used. |
Beta Was this translation helpful? Give feedback.
-
You can use python higher order functions for this use case; this doc will help you example code:
|
Beta Was this translation helpful? Give feedback.
-
Hi guys, I made a package to make this easily reproducible amongst other packages, it's called pyzeus and works for both sync and async route handlers, as well as on a the APIRouter dependencies array as you see fit; It has default settings but you can override them with kwargs Here's an example of the usage. Also, no need to worry about using pydantic it works the same in case you are using a pydantic model or models for your handlers. Router InstanceThis implementation is equally from pyzeus import zeus
from fastapi import APIRouter, Depends
router = APIRouter(dependencies=[Depends(zeus().thunder)])
@router.get("/")
async def hander():
return { "message": "lorem ipsum" }
@router.options("/")
async def options_hander():
return None Specific Routefrom pyzeus import zeus
from fastapi import APIRouter, Depends
router = APIRouter()
# Synchronous example
@router.get("/")
@zeus().smite
def synchronous_handler(request: Request, response: Response):
return { "message": "lorem ipsum" }
# Asynchronous example
@router.get("/")
@zeus().smite
async def asynchronous_handler(request: Request, response: Response):
return { "message": "lorem ipsum" }
# Pydantic example
class Item(BaseModel):
name: str
@router.post("/")
@zeus().smite
async def asynchronous_handler(request: Request, response: Response, item: Item):
return { "message": item } |
Beta Was this translation helpful? Give feedback.
-
As of FastAPI 0.108.0, the APIRouter has the Given the increase of granularity, we can now have middlewares per route or router. |
Beta Was this translation helpful? Give feedback.
-
Hello everyone, I don't know if the discussion is still about router- or route-based middleware. I have tested it and it worked well for me. I would love to know if this helped anyone. |
Beta Was this translation helpful? Give feedback.
-
First check
Description
Is it possible to use different middleware for different routes/path?
Additional context
In my case my need comes from CORS. But, I am sure there is other cases than CORS requirements that someone would need different middlewares for different paths.
myapi.com/path1
to allow origins of calls frommyfrontend.com
myapi.com/path2
to allow origins of calls from anywhere ('*') since it is a public facing api.I checked if it was possible to add a middleware at a router level and did not find any documentation about it.
Code example
Beta Was this translation helpful? Give feedback.
All reactions