Browse Source

🐛 Fix bug, allow empty path in path operation in prefixless router (#15763)

pull/15765/head
Sebastián Ramírez 5 days ago
committed by GitHub
parent
commit
d8aad201eb
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      fastapi/routing.py
  2. 55
      tests/test_router_include_context.py

13
fastapi/routing.py

@ -2435,9 +2435,16 @@ class APIRouter(routing.Router):
"A path prefix must not end with '/', as the routes will start with '/'" "A path prefix must not end with '/', as the routes will start with '/'"
) )
else: else:
for r in _iter_included_route_candidates(router.routes): for route, route_context in _iter_routes_with_context(router.routes):
path = getattr(r, "path", None) if route_context is None:
name = getattr(r, "name", "unknown") path = getattr(route, "path", None)
name = getattr(route, "name", "unknown")
elif route_context.starlette_route is not None:
path = getattr(route_context.starlette_route, "path", None)
name = getattr(route_context.starlette_route, "name", "unknown")
else:
path = route_context.path
name = route_context.name
if path is not None and not path: if path is not None and not path:
raise FastAPIError( raise FastAPIError(
f"Prefix and path cannot be both empty (path operation: {name})" f"Prefix and path cannot be both empty (path operation: {name})"

55
tests/test_router_include_context.py

@ -2,6 +2,7 @@ from typing import Annotated, cast
import pytest import pytest
from fastapi import APIRouter, Body, Depends, FastAPI, Request from fastapi import APIRouter, Body, Depends, FastAPI, Request
from fastapi.exceptions import FastAPIError
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
from fastapi.routing import ( from fastapi.routing import (
APIRoute, APIRoute,
@ -807,6 +808,60 @@ def test_no_prefix_include_validation_sees_effective_starlette_route_candidates(
assert cast(Route, candidates[0]).path == "/child/items" assert cast(Route, candidates[0]).path == "/child/items"
def test_no_prefix_include_validation_sees_effective_api_route_path():
leaf_router = APIRouter()
@leaf_router.get("")
def read_items():
return []
parent_router = APIRouter()
parent_router.include_router(leaf_router, prefix="/items")
# for coverage
candidates = list(_iter_included_route_candidates(parent_router.routes))
assert cast(APIRoute, candidates[0]).path == ""
app = FastAPI()
app.include_router(parent_router)
client = TestClient(app)
response = client.get("/items")
assert response.status_code == 200, response.text
assert response.json() == []
def test_no_prefix_include_validation_sees_effective_starlette_route_path():
def endpoint(request):
return PlainTextResponse("ok")
child_router = APIRouter(routes=[Route("/items", endpoint, name="read_items")])
parent_router = APIRouter()
parent_router.include_router(child_router, prefix="/child")
app = FastAPI()
app.include_router(parent_router)
client = TestClient(app)
response = client.get("/child/items")
assert response.status_code == 200, response.text
assert response.text == "ok"
def test_no_prefix_include_validation_rejects_empty_effective_api_route_path():
router = APIRouter()
@router.get("")
def read_items(): # pragma: no cover
return []
app = FastAPI()
with pytest.raises(FastAPIError):
app.include_router(router)
def test_apirouter_matches_fallback_without_include_context(): def test_apirouter_matches_fallback_without_include_context():
router = APIRouter() router = APIRouter()

Loading…
Cancel
Save