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

This commit is contained in:
Sebastián Ramírez
2026-06-15 12:55:06 +02:00
committed by GitHub
parent 9a9c4ad5d0
commit d8aad201eb
2 changed files with 65 additions and 3 deletions

View File

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

View File

@@ -2,6 +2,7 @@ from typing import Annotated, cast
import pytest
from fastapi import APIRouter, Body, Depends, FastAPI, Request
from fastapi.exceptions import FastAPIError
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
from fastapi.routing import (
APIRoute,
@@ -807,6 +808,60 @@ def test_no_prefix_include_validation_sees_effective_starlette_route_candidates(
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():
router = APIRouter()