-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
✨ Prioritize static routes to avoid route shadowing of dynamic routes #14052
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
base: master
Are you sure you want to change the base?
Conversation
📝 Docs preview for commit 02255a4 at: https://5bf6901a.fastapitiangolo.pages.dev Modified Pages |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@deadlovelll, thanks for your interest!
I also tried solving this problem, with no success. The thing is that there are lots of edge cases we need to consider..
In this implementation reorder_routes_on_startup
is called in the constructor (__init__()
) of the app, so it actually only sorts internal automatically added routes ('/openapi.json', '/docs', '/docs/oauth2-redirect', '/redoc') - all user-defined routes are added later and they are not reordered.
Just run this test that is created from the example from tutorial001
:
(see in the details)
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
# Dynamic route
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
# Static route
@app.get("/items/stats")
async def read_stats():
return {"status": "ok"}
def test_():
client = TestClient(app)
resp = client.get("/items/stats")
assert resp.status_code == 200 # assert 422 == 200
That's why we need to cover all code examples from docs by tests.. 🤓
For now to make it working you need to manually call app.reorder_routes_on_startup()
after adding all routes.
Just a couple of use cases that are not handled in your implementation (there are more of them):
- Route
{path:path}
parameter will shadow all routes with more than 1 path parameter:
@app.get("/case1/{item_id}/{action}/")
def act(item_id: int, action: str):
return {"item_id": item_id, "action": action}
@app.get("/case1/{path:path}")
def default(path: str):
return {"path": path}
- imagine we want to handle int
item_id
s separately from str ones:
@app.get("/case2/{item_id:int}/{action}/")
def int_ids(item_id: int, action: str):
return {"route": "int_ids"}
@app.get("/case2/{item_id:str}/add/")
def str_ids_add(item_id: str):
return {"route": "str_ids_add"}
@app.get("/case2/{item_id:str}/{action}/")
def str_ids_default(item_id: str, action: str):
return {"route": "str_ids_default"}
In both these cases reordering will brake the app..
See tests in the details
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
# 1
@app.get("/case1/{item_id}/{action}/")
def act(item_id: int, action: str):
return {"item_id": item_id, "action": action}
@app.get("/case1/{path:path}")
def default(path: str):
return {"path": path}
# 2
@app.get("/case2/{item_id:int}/{action}/")
def int_ids(item_id: int, action: str):
return {"route": "int_ids"}
@app.get("/case2/{item_id:str}/add/")
def str_ids_add(item_id: str):
return {"route": "str_ids_add"}
@app.get("/case2/{item_id:str}/{action}/")
def str_ids_default(item_id: str, action: str):
return {"route": "str_ids_default"}
app.reorder_routes_on_startup()
# Tests
client = TestClient(app)
# 1
def test_case1_add():
resp = client.get("/case1/1/add/")
assert resp.status_code == 200, resp.json()
assert resp.json() == {"item_id": 1, "action": "add"}
def test_case1_default():
resp = client.get("/case1/something/")
assert resp.status_code == 200, resp.json()
assert resp.json() == {"path": "something/"}
# 2
def test_case2_int():
resp = client.get("/case2/1/add/")
assert resp.status_code == 200, resp.json()
assert resp.json() == {"route": "int_ids"}
def test_case2_str_add():
resp = client.get("/case2/asd/add/")
assert resp.status_code == 200, resp.json()
assert resp.json() == {"route": "str_ids_add"}
def test_case2_str_other():
resp = client.get("/case2/asd/other/")
assert resp.status_code == 200, resp.json()
assert resp.json() == {"route": "str_ids_default"}
Thank you for your feedback) i will check it and make some work under my mistakes |
Automatic Route Reordering in FastAPI
Problem
Static routes (e.g.,
/items/stats
) were sometimes being matched by dynamic routes like/items/{item_id}
.This caused path parameter validation to fail and returned HTTP 422, instead of hitting the intended static handler.
Root cause:
Starlette/FastAPI matches routes in the order they are registered. Dynamic routes could be registered before their static siblings, leading to static routes being shadowed.
Impact
Solution
{}
) are prioritized first.Benefits
Reproduction Steps
1. Save the following code as
main.py
:2. Run the application:
3. Test the dynamic route:
Response:
HTTP/1.1 200 OK {"item_id":123}
4. Test the static route:
Response before fix (without route reordering):
Response after fix (with automatic route reordering):
5. Does this affect application startup time?
No, the impact on startup is negligible. You can verify this with the following benchmark:
Result:
Its only costs 1ms per 100 routes