Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use different middleware for different routes/path #1174

Closed
3 tasks done
philippegirard opened this issue Mar 27, 2020 · 23 comments · May be fixed by #5897
Closed
3 tasks done

How to use different middleware for different routes/path #1174

philippegirard opened this issue Mar 27, 2020 · 23 comments · May be fixed by #5897
Labels
question Question or problem question-migrate

Comments

@philippegirard
Copy link

philippegirard commented Mar 27, 2020

First check

  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the FastAPI documentation, with the integrated search.
  • I already searched in Google "How to X in FastAPI" and didn't find any information.

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.

  • I want myapi.com/path1 to allow origins of calls from myfrontend.com
  • I want 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

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=['myfrontend.com'],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# I want /path1 to allow only calls from 'myfrontend.com'
@app.get("/path1")
async def path1():
    return {"status": "alive"}

# I want /path2 to be a public facing api that can be accessed from anywhere ['*']
@app.get("/path2")
async def path2():
    return {"status": "alive"}
@philippegirard philippegirard added the question Question or problem label Mar 27, 2020
@Dustyposa
Copy link
Contributor

I haven't use this way, but you can have a try.
How about use subapp?
documention

@philippegirard
Copy link
Author

philippegirard commented Mar 28, 2020

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"}

@tiangolo
Copy link
Owner

tiangolo commented Apr 6, 2020

Thanks for your help here @Dustyposa 🙇‍♂️ 🍰

And thanks for reporting back and closing the issue @philippegirard 🚀

@mreschke
Copy link

mreschke commented Feb 3, 2021

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 router.get('/path', middleware=['abc', 'xyz'] etc...

@HediHargam
Copy link

HediHargam commented Apr 15, 2021

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")
async def CSPMiddleware(request: Request, call_next):

response = await call_next(request)
response.headers[
    "Content-Security-Policy"
] = "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline';"
return response

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)
nb2: app.mount does not solve the problem because swaggers are not available for subapps

@adriangb
Copy link
Contributor

+1 for router and/or route level middleware

@tiago-lp
Copy link

tiago-lp commented Aug 25, 2021

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)

@adriangb
Copy link
Contributor

An interesting alternative is to use customization of the APIRoute class: https://fastapi.tiangolo.com/advanced/custom-request-and-route/#custom-apiroute-class-in-a-router

I wonder if we could wrap an existing middleware into an APIRouter to get something like:

router = APIRouter(route_class=SomeWrapper(middleware=[Middleware1, Middleware2]))

@adriangb
Copy link
Contributor

adriangb commented Aug 25, 2021

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 APIRouter and/or APIRoute, but I wanted to do this without subclassing APIRouter.

@adriangb
Copy link
Contributor

adriangb commented Sep 13, 2021

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.

@adriangb
Copy link
Contributor

encode/starlette#1286 is implemented at the route level, so it should even be possible to do something like:

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).

@abhaynayak24
Copy link

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("/app1", app1) # if does not work try: app.mount(app2, "/app2")

@app.get("/")
async def root():
    return {"message": "alive"}

@philippegirard which middleware would execute first by default? Can we control the order of execution of the middlewares?

@philippegirard
Copy link
Author

philippegirard commented Dec 5, 2021

@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 :) !

@josebrz
Copy link

josebrz commented Dec 7, 2021

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

routes_with_middleware = [
    "/api/endpoint"
]

async def middleware(request: Request, call_next):
    if request.url.path not in routes_with_middleware:
        return await call_next(request)
   else:
        ....

In this way, if your middleware does validations, they do not block the other routes such as the api / docs for example

@dangleh
Copy link

dangleh commented Dec 10, 2021

I haven't use this way, but you can have a try. How about use subapp? documention

Hi, the document has moved to a new link .
Thank you for the recommendation 😄

@jeanlst
Copy link

jeanlst commented Jan 11, 2022

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?

@adriangb
Copy link
Contributor

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.

@IVIyg0t
Copy link

IVIyg0t commented Mar 9, 2022

Any news here? I have a need for route specific middleware.

@gareth-leake
Copy link

gareth-leake commented May 16, 2022

Also would love to see route level middleware.

EDIT: Current work on this is pending, here: Starlette PR#1464

@adriangb
Copy link
Contributor

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.

@piyushere
Copy link

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.
Even though you can't define a middleware for a router, you can always have a router dependency, (instead of adding it on every individual route), And it very much works like any middleware.

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')
# ...

@tizzo
Copy link

tizzo commented Feb 22, 2023

@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.

@adriangb
Copy link
Contributor

I think that yes a PR to FastAPI would be accepted

@tiangolo tiangolo changed the title [QUESTION] How to use different middleware for different routes/path How to use different middleware for different routes/path Feb 24, 2023
@tiangolo tiangolo reopened this Feb 28, 2023
Repository owner locked and limited conversation to collaborators Feb 28, 2023
@tiangolo tiangolo converted this issue into discussion #7691 Feb 28, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Question or problem question-migrate
Projects
None yet