8000 ✨ Add ignore trailing slashes option to `FastAPI` class by Synrom · Pull Request #12145 · fastapi/fastapi · GitHub
[go: up one dir, main page]

Skip to content

✨ Add ignore trailing slashes option to FastAPI class #12145

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

Open
wants to merge 15 commits into
base: master
Choose a base branch
from

Conversation

Synrom
Copy link
@Synrom Synrom commented Sep 6, 2024

Add a boolean flag ignore_trailing_slash to FastAPI class that decides whether to ignore trailing slashes at the end of URIs.

For example, by setting ignore_trailing_slash to True, requests to /auth and /auth/ will have the same behaviour.
By default (ignore_trailing_slash is False), the two requests are treated differently and one of them will result in a 307-redirect.

Using the flag is as easy as:

app = FastAPI(ignore_trailing_slash=True)

This change provides a simple solution to discussions #9328 and #7298

@svlandeg svlandeg changed the title ✨ Add ignore trailing slashes option to FastAPI class ✨ Add ignore trailing slashes option to FastAPI class Sep 6, 2024
8000
@svlandeg svlandeg added the feature New feature or request label Sep 6, 2024
Copy link
Member
@svlandeg svlandeg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! We'll get this reviewed and will get back to you 🙏

@svlandeg
Copy link
Member
svlandeg commented Sep 6, 2024

Also FYI - the CI is currently failing due to an unrelated issue - we're looking into it.

Update: The CI has been fixed by merging in the latest from master, but there is a remaining issue with this PR not reaching 100% coverage. What this means is that there are new code paths in this PR that are not yet checked by the test suite - probably one of the if conditions that never hits. Ideally, we should extend the tests so that the coverage goes back to 100%.

@Synrom
Copy link
Author
Synrom commented Sep 8, 2024

The code-coverage is at 100% now. I added the tests before removing code duplication to check that all if conditions were covered.

@Synrom Synrom requested a review from svlandeg September 10, 2024 12:07
Copy link
Member
@svlandeg svlandeg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks again for the PR!

I've now had a more detailed look at the proposal here.

From going through old issues and discussions, it does seem like a lot of users are looking for ways to unify the behaviour between paths with and without slashes. It looks like this used to be possible with /? syntax but that isn't supported anymore.

You can still obtain this behaviour by registering the path in the router twice, once with slash and once without, but I can see how that gets unweildy for large applications.

In the current implementation on master, suppose you have something like

@router.get('/hello')
def hello_page() -> str:
    return 'Hello, World!'

Then when accessing http://127.0.0.1:8000/hello/ you would get a

"GET /hello/ HTTP/1.1" 307 Temporary Redirect

However if you've specified ignore_trailing_slash=True with this PR, you'd get

"GET /hello HTTP/1.1" 200 OK

i.e. the trailing slash is already removed before it hits the server & the logs and the behaviour is indeed exactly the same.

One issue with the current implementation is that users wouldn't be warned if the app does register two variants of the path, e.g.

@router.get('/hello')
def hello_page() -> str:
    return 'Hello, World!'

@router.get('/hello/')
def hello_page() -> str:
    return 'Hello, Slashed World!'

With FastAPI(ignore_trailing_slash=True), the second path /hello/ would never get called.

So to recap, I can see how this ignore_trailing_slash=True behaviour could be preferred in some applications, but it does add some complexity and potential confusion on top of the existing functionality with redirect_slashes etc. If we do want this feature, we need to add some documentation to it as well, to make sure the consequences of using this flag are crystal clear.

I'm going to leave the final decision up to the project's BDFL, Mister Tiangolo 😁

@Synrom
Copy link
Author
Synrom commented Sep 15, 2024

Thanks again for the PR!

I've now had a more detailed look at the proposal here.

From going through old issues and discussions, it does seem like a lot of users are looking for ways to unify the behaviour between paths with and without slashes. It looks like this used to be possible with /? syntax but that isn't supported anymore.

You can still obtain this behaviour by registering the path in the router twice, once with slash and once without, but I can see how that gets unweildy for large applications.

In the current implementation on master, suppose you have something li 8000 ke

@router.get('/hello')
def hello_page() -> str:
    return 'Hello, World!'

Then when accessing http://127.0.0.1:8000/hello/ you would get a

"GET /hello/ HTTP/1.1" 307 Temporary Redirect

However if you've specified ignore_trailing_slash=True with this PR, you'd get

"GET /hello HTTP/1.1" 200 OK

i.e. the trailing slash is already removed before it hits the server & the logs and the behaviour is indeed exactly the same.

One issue with the current implementation is that users wouldn't be warned if the app does register two variants of the path, e.g.

@router.get('/hello')
def hello_page() -> str:
    return 'Hello, World!'

@router.get('/hello/')
def hello_page() -> str:
    return 'Hello, Slashed World!'

With FastAPI(ignore_trailing_slash=True), the second path /hello/ would never get called.

So to recap, I can see how this ignore_trailing_slash=True behaviour could be preferred in some applications, but it does add some complexity and potential confusion on top of the existing functionality with redirect_slashes etc. If we do want this feature, we need to add some documentation to it as well, to make sure the consequences of using this flag are crystal clear.

I'm going to leave the final decision up to the project's BDFL, Mister Tiangolo 😁

Thanks for your detailed feedback on the pull request.

Perhaps a quick note that the behavior of ignore_trailing_slash=True when /hello and /hello/ are registered one after the other would be the same as registering /hello twice. In both cases, there would be no warning and the the second registration is ignored.

I added a bit more documentation to provide clarity on how the proposed flag influences the behavior when multiple variants of the same path are registered.

@tiangolo , what do you think?

@iudeen
Copy link
Contributor
iudeen commented Sep 19, 2024

It would be interesting to see how this will work in combination with another PR #5595 that validates duplicate paths.

@Synrom
Copy link
Author
Synrom commented Sep 19, 2024

It would be interesting to see how this will work in combination with another PR #5595 that validates duplicate paths.

Good point, I just looked at PR #5595. It looks to me like it will integrate well with this one.

We just have to be careful to strip the trailing slashes before calculating the route hashes.

@Synrom
Copy link
Author
Synrom commented Oct 6, 2024

@tiangolo Any updates on this?

@brianoflondon
Copy link

I just want to add a comment as to where this becomes vitally important from what I encountered.

When I'm using Cloudflare forwarding to Traefik as a reverse proxy to forward to FastAPI there is a huge fundamental difference in the way IP's are passed for HTTPS and HTTP: the 307 redirect breaks this and causes things to be very much more complicated when trying to put in place rate limits and other IP specific things.

This is fixed outside of FastAPI with

uvicorn --proxy-headers --forwarded-allow-ips=*

or

# This was added with refrence to this link: https://github.com/tiangolo/fastapi/discussions/9328#discussioncomment-8242245
CMD ["gunicorn", "v4vapp_api_ext.main:app", "--proxy-protocol", "--forwarded-allow-ips=*", "--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:80"]

But trailing slashes and 307 redirects are definitely an issue.

@gitHoussem
Copy link

The entire web community are waiting for this PR to be merged a new version gets released ! Thank you

@aayushchhabra1999
Copy link
Contributor
aayushchhabra1999 commented Nov 18, 2024

+1 for this. Getting this merged would really help! CC: @tiangolo

@alexjrk
Copy link
alexjrk commented Nov 18, 2024

+1 it would be perfect 😄 cc @tiangolo

@svlandeg
Copy link
Member
svlandeg commented Nov 19, 2024

Hi all! Thanks for the interest in this PR 🙏

It's better to just "upvote" the original post and/or show it some love with a 👍, 🎉 or ❤️. Just commenting "+1" and tagging maintainers adds quite a bit of noise to our notifications, actually slowing down maintenance on the OS repos rather than helping 😉 That said, of course detailed reviews and comments are always welcome.

And just to recap - we've got this internally on our queue for Tiangolo to review. Thanks for your patience!

@luzzodev
Copy link
luzzodev commented Nov 19, 2024

First i want to say thanks to @Synrom for the contribution and to @svlandeg to follow this PR.

This is my personal opinion on the PR: while on one hand, I approve of the choice and elegance of using middleware to simplify this kind of operation, I believe that the approach of integrating it directly as an option in FastAPI is wrong for a number of reasons.

  1. Confusion: as mentioned by @svlandeg Fastapi already support redirect_slashes ( i think that even this is a bad implementation but is comes from starlette) and adding another option to address the same 'issue' in another way is not good and can create a lot of cofusion.
  2. What i like of Fastapi is that is pluggable so you can customize quite all what you need on top of that and you can reuse all componens whout having to rewrite it. With this is mind, in your implementation i'm forced to rewrite a traling slash middleware if need to resuse it in a different way.
  3. Another foundamental point is that adding the trailing-slash middleware as option ignore_trailing_slash in Fastapi will imply a fixed position for the trailing-slash middleware and this will prevent a full customization of the middleware execution order.
    • Example: i would like to use the trailing-slash but first i would run another middleware that collect the real path of a request.

What i propose:

Make the RemovingTrailingSlashMiddleware available through the middlewares defined or exported by FastAPI, create dedicated documentation for the middleware, and use it as a reference for an example context as requested by many.

At the end to solve the current 'issue' ( that for me is more an utility that an issue) just few line of code are required.

class RemoveTrailingSlashMiddleware:

    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope, receive, send):
        # Ensure we are working with an HTTP or WebSocket request
        if scope["type"] in {"http", "websocket"}:
            path = scope["path"]

            # Remove trailing slash for all paths except the root "/"
            if path != "/" and path.endswith("/"):
                scope["path"] = path.rstrip("/")

        # Call the next middleware or app in the stack
        await self.app(scope, receive, send)

And add the middleware to the app with the disired order.

app.add_middleware(RemoveTrailingSlashMiddleware)

@Synrom
Copy link
Author
Synrom commented Nov 19, 2024

Hey @luzzodev,

thanks for the reply and feedback.
I actually used a middleware in the first place, because I thought it would fit in nicely with FastAPI's philosophy.
The alternative would be to simply add an if-condition in the FastAPI.__call__ method.

Your criticism about using a middleware is valid.
I thought about separating _IgnoreTrailingWhitespaceMiddleware from the FastAPI class.
But that would be a bit misleading, because using the class by itself is very prone to errors (more about that later).
I don't know what design decision is best here. Im open to suggestions.

Now about your proposal of just introducing a RemoveTrailingSlashMiddleware.
I want to highlight a technical limitation of using middleware alone: it does not adjust the registered URIs.
Consider the following example:

app = FastAPI()

app.add_middleware(RemoveTrailingSlashMiddleware)

app.get("/auth/")
def return_msg():
    return {"msg": "This is unreachable"}

In this scenario, both /auth/ and /auth would trigger an infinite loop of temporary redirects.
This occurs because the RemoveTrailingSlashMiddleware modifies the incoming path by removing the trailing slash, which causes FastAPI to issue a redirect.
Consequently, without changing the registered URIs, certain endpoints can become unreachable.

I believe this issue has captured a lot of interest because many users share a similar experience: their FastAPI application runs perfectly in a local environment, but once it's deployed behind a complex proxy setup, things break due to temporary redirects.
The idea behind this implementation is to simplify things from the user's perspective.
You just need to set a flag and you don't get any more temporary redirects due to trailing slashes.

@luzzodev
Copy link

Hey @luzzodev,

thanks for the reply and feedback. I actually used a middleware in the first place, because I thought it would fit in nicely with FastAPI's philosophy. The alternative would be to simply add an if-condition in the FastAPI.__call__ method.

Your criticism about using a middleware is valid. I thought about separating _IgnoreTrailingWhitespaceMiddleware from the FastAPI class. But that would be a bit misleading, because using the class by itself is very prone to errors (more about that later). I don't know what design decision is best here. Im open to suggestions.

Now about your proposal of just introducing a RemoveTrailingSlashMiddleware. I want to highlight a technical limitation of using middleware alone: it does not adjust the registered URIs. Consider the following example:

app = FastAPI()

app.add_middleware(RemoveTrailingSlashMiddleware)

app.get("/auth/")
def return_msg():
    return {"msg": "This is unreachable"}

In this scenario, both /auth/ and /auth would trigger an infinite loop of temporary redirects. This occurs because the RemoveTrailingSlashMiddleware modifies the incoming path by removing the trailing slash, which causes FastAPI to issue a redirect. Consequently, without changing the registered URIs, certain endpoints can become unreachable.

I believe this issue has captured a lot of interest because many users share a similar experience: their FastAPI application runs perfectly in a local environment, but once it's deployed behind a complex proxy setup, things break due to temporary redirects. The idea behind this implementation is to simplify things from the user's perspective. You just need to set a flag and you don't get any more temporary redirects due to trailing slashes.

Hi @Synrom just to clarify some points:

  • I'm on the same team about middleware choise, i just think that make it reusable would be a lot better.
  • About your example i would go a bit deeper:
    • While offering a ready to go feature is a cool and good thing, is even better to avoid and discourage bad practices. In this case mixing path definitions using slash some times and sometims not is of course really bad practice if they are used with the same meaning. Just keep in mind that in reality they are different path.
    • Even in the example you provided, the approach is wrong because the methods are being mixed together. And this is a bad practice to discourage and of course to mention in the possible middleware documentation.
    • Another thing to distinguish, is the functionality itself from the error checking to prevent possible unwanted behaviour. In other words on one hand you have the middleware, on the other the possibility to check possible conflics for bad configuration.
      • Ex when you include a router prefix and path cound't be both empty. In this case redirect_slashes should b 9E88 e disabled if you add the TrailingMiddleware, or you can check that your routes do not ends with /.

What i'm bringing on the table are just example from a different prospective and just confirm how bad was the choise to to add redirect_slashes in starlette to True by default.

The real point here is that treat /hello-world and /hello-world/ as the same endpoint could be a nice feature to have but nothing more and that should not encourage to write mixed bad endpoints path.

In conclusion, i like your solution and your work but fixing the middleware execution position, not reusable middleware and possible confusion with already implemented redirect_slashes for me a problems that need to be solved.

@tusharsnx
Copy link
tusharsnx commented Nov 26, 2024

@Synrom

While offering a ready to go feature is a cool and good thing, is even better to avoid and discourage bad practices. In this case mixing path definitions using slash some times and sometims not is of course really bad practice if they are used with the same meaning. Just keep in mind that in reality they are different path.

In FastAPI, /path/ and /path/ are equivalent. This is evident from the current behavior of FastAPI, which redirects /path to /path/ when it cannot find the exact match for the path. On the contrary, it would've been better if FastAPI never had a 'redirect' solution for paths like /path/ since that would've failed both in production and locally.

Our problem is how the equivalency is solved at runtime. Redirect helps but it fails in a number of situations including when FastAPI is behind a (simple) proxy (that handles TLS and only forwards non-HTTPS requests to the server). Redirect has it own problems as well, e.g. latency. More redirects = Higher latency. It gets worse if commonly used paths have to go through this issue. So, instead of redirects, it's much better to just ignore trailing slashes and resolve both /path and /path/ to the same route handler.

Make the RemovingTrailingSlashMiddleware available through the middlewares defined or exported by FastAPI, create dedicated documentation for the middleware, and use it as a reference for an example context as requested by many.

I don't understand how RemoveTrailingSlashMiddleware could help here, since it makes it worse when even a good path like /path/ will have its trailing slash stripped away. So, the journey of /path/ will become:

`/path/` -> (run RemoveTrailingSlashMiddleware) -> `/path` -> (fastapi redirects it back because `/path` doesn't exist but `/path/` does)

We arrived at the same situation even for the right path.

I guess we require AddTrailingSlashMiddleware which instead adds a trailing slash when the path doesn't exist and does a second chance checking before returning a 404.

@luzzodev
Copy link

Dear @tusharsnx, with your response, you are just giving me more evidence of how much confusion there is.

In FastAPI, /path/ and /path/ are equivalent. This is evident from the current behavior of FastAPI, which redirects /path to /path/ when it cannot find the exact match for the path.

First of all FastApi has nothing to do with redirect, is a Starlette implementation instead. Fastapi just wrap over Starlette and, for consistency, expose the same arguments keeping default unchanged.

The evidence are really the opposite of the ones you are describing, because indeed a redirect is called instead of a direct execution of the new path and even in the match phase of a route, there is no trace of considering the two path equivalent. This is not only related to Fastapi/Starlette but is how it works in the majority of the cases (Web Server, Proxies, etc.).
The intent of redirect_slashes=True like all other implementations is to define a kind of fallback that is far away from be equivalent. More on redirect and why they are not equivalent later.

On the contrary, it would've been better if FastAPI never had a 'redirect' solution for paths like /path/ since that would've failed both in production and locally.

I could partially agree with it, in a lot of implementations, proxy, webserver etc. this is default behavior but in a context where you have direct control over path definition could be a little tricky. The problem about failing locally and in production could be for sure addressable to other things like having the follow redirect enabled by default on most http client.

Our problem is how the equivalency is solved at runtime. Redirect helps but it fails in a number of situations including when FastAPI is behind a (simple) proxy (that handles TLS and only forwards non-HTTPS requests to the server). Redirect has it own problems as well, e.g. latency. More redirects = Higher latency. It gets worse if commonly used paths have to go through this issue. So, instead of redirects, it's much better to just ignore trailing slashes and resolve both /path and /path/ to the same route handler.

Your problem is the same of many others that are dealing with trailing slash and do not want to handle redirect.
The reality is that /path/ and /path should never be considered equal and this is valid in proxies, caches and so on, but you can always customize it if you want.
If you use a proxy to cache responses how do you think it will handle caching?
If you hit /path/ or /path proxy will cache the responses not under the same uri and this will cause less cache it or double the size of the cache and in general this is a bigger problem than latency of a redirect. Instead the redirect is the way to go to grant more flexibility. With redirect only the real uri responses will be cached.

In conclusion if you want more flexibility then go with redirect and let redirect_slashes=True if you need to be more strict then set redirect_slashes=False. Those options should be the only two allowed in Fastapi like already are.
On the other hand, i understand that, for a lot of people, the feature of threat /path/ and /path in the same way could be really useful and they have already considered the trade-off with proxy and cache but this should comes with consistency.

What i mean for consistency?

Define the presence, format, and semantics of a path component in URIs.
If you are defining the equivalence between /path and /path/ then only one format should be allowed with or without trailing slash. By having mixed path (with and without /) you are violating the assumption that /path and /path/are equivalent.

But maybe even this approach is not enough because be consistent is too strict and in reality you, and many others, want to process all routes with a king of "resolve_internal_redirect" option. As i said, this could be easily achieved by using a middleware that, of course, is not the one i wrote and it was not intended for it.

Make the RemovingTrailingSlashMiddleware available through the middlewares defined or exported by FastAPI, create dedicated documentation for the middleware, and use it as a reference for an example context as requested by many.

I don't understand how RemoveTrailingSlashMiddleware could help here, since it makes it worse when even a good path like /path/ will have its trailing slash stripped away. So, the journey of /path/ will become:

`/path/` -> (run RemoveTrailingSlashMiddleware) -> `/path` -> (fastapi redirects it back because `/path` doesn't exist but `/path/` does)

We arrived at the same situation even for the right path.

I guess we require AddTrailingSlashMiddleware which instead adds a trailing slash when the path doesn't exist and does a second chance checking before returning a 404.

What i described is just one simple way that you can use to achieve the result of treat equally uri with and without trailing slash but of course keeping consistency and your example you were not consistent.
As i said before i suppose you want something more related to resolve_internal_redirect and it not the intent of the example above.

@Synrom
Copy link
Author
Synrom commented Nov 27, 2024

Dear @luzzodev,

I really like the idea of having a flag to resolve redirects internally. That said, I believe this functionality would be better implemented directly in Starlette rather than in FastAPI.

FastAPI relies on the Starlette Router under the hood, and it’s the Router that issues the redirect responses. A simple solution would be to introduce a flag in the Router class to enable internal resolution of redirects.
That flag can then be used by FastAPI.

I've started a discussion on the Starlette repository about this—feel free to check it out! 😄

@addy999
Copy link
addy999 commented Feb 27, 2025

@Synrom I agree with it being in Starlette. But I think at the very least, this should be called out in the documentation for those unfamiliar with the default behaviour. I just spent hours chasing down HTTPS issues on the client side because fastapi was doing this redirect silently.

@gaby
Copy link
gaby commented Apr 3, 2025

@luzzodev @Synrom The discussion in starlette was shutdown immediately. Is the plan to add this to FastAPI then?

@luzzodev
Copy link
luzzodev commented Apr 3, 2025

@luzzodev @Synrom The discussion in starlette was shutdown immediately. Is the plan to add this to FastAPI then?

I don't have any information about it, but I personally agree with what was said in starlette by @Kludex .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

0