mirror of
https://github.com/fastapi/fastapi.git
synced 2026-09-11 12:57:56 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
866b7a3d0c | ||
|
|
7b3effea75 | ||
|
|
7fe315c21a |
No files matched your search
@@ -7,6 +7,12 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
## 0.139.2 (2026-07-16)
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🐛 Refactor router route building to make it thread-safe, mainly relevant for tests running in parallel threads (uncommon). PR [#16013](https://github.com/fastapi/fastapi/pull/16013) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.139.1 (2026-07-16)
|
||||
|
||||
### Fixes
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.139.1"
|
||||
__version__ = "0.139.2"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
+46
-33
@@ -7,6 +7,7 @@ import inspect
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import threading
|
||||
import types
|
||||
from collections.abc import (
|
||||
AsyncIterator,
|
||||
@@ -1571,6 +1572,9 @@ class RouteContext:
|
||||
class _IncludedRouter(BaseRoute):
|
||||
original_router: "APIRouter"
|
||||
include_context: _RouterIncludeContext
|
||||
_effective_routes_lock: Any = field(
|
||||
default_factory=threading.Lock, repr=False, compare=False
|
||||
)
|
||||
_effective_candidates: list["_EffectiveRouteContext | _IncludedRouter"] = field(
|
||||
default_factory=list
|
||||
)
|
||||
@@ -1584,44 +1588,53 @@ class _IncludedRouter(BaseRoute):
|
||||
routes_version = self.original_router._get_routes_version()
|
||||
if routes_version == self._effective_candidates_version:
|
||||
return self._effective_candidates
|
||||
self._effective_candidates = []
|
||||
candidates = self.original_router.routes
|
||||
for route in candidates:
|
||||
if isinstance(route, _IncludedRouter):
|
||||
child_context = self.include_context.combine(route.include_context)
|
||||
child_branch = _IncludedRouter(
|
||||
original_router=route.original_router,
|
||||
include_context=child_context,
|
||||
)
|
||||
self._effective_candidates.append(child_branch)
|
||||
continue
|
||||
route_context = self._build_effective_context(route)
|
||||
if route_context is not None:
|
||||
self._effective_candidates.append(route_context)
|
||||
self._effective_candidates_version = routes_version
|
||||
return self._effective_candidates
|
||||
with self._effective_routes_lock:
|
||||
routes_version = self.original_router._get_routes_version()
|
||||
if routes_version == self._effective_candidates_version:
|
||||
return self._effective_candidates
|
||||
effective_candidates: list[_EffectiveRouteContext | _IncludedRouter] = []
|
||||
for route in self.original_router.routes:
|
||||
if isinstance(route, _IncludedRouter):
|
||||
child_context = self.include_context.combine(route.include_context)
|
||||
child_branch = _IncludedRouter(
|
||||
original_router=route.original_router,
|
||||
include_context=child_context,
|
||||
)
|
||||
effective_candidates.append(child_branch)
|
||||
continue
|
||||
route_context = self._build_effective_context(route)
|
||||
if route_context is not None:
|
||||
effective_candidates.append(route_context)
|
||||
self._effective_candidates = effective_candidates
|
||||
self._effective_candidates_version = routes_version
|
||||
return effective_candidates
|
||||
|
||||
def effective_low_priority_routes(self) -> list["_EffectiveRouteContext"]:
|
||||
routes_version = self.original_router._get_routes_version()
|
||||
if routes_version == self._effective_low_priority_routes_version:
|
||||
return self._effective_low_priority_routes
|
||||
self._effective_low_priority_routes = []
|
||||
for route in self.original_router._low_priority_routes:
|
||||
route_context = self._build_effective_context(route)
|
||||
if route_context is not None:
|
||||
self._effective_low_priority_routes.append(route_context)
|
||||
for route in self.original_router.routes:
|
||||
if isinstance(route, _IncludedRouter):
|
||||
child_context = self.include_context.combine(route.include_context)
|
||||
child_branch = _IncludedRouter(
|
||||
original_router=route.original_router,
|
||||
include_context=child_context,
|
||||
)
|
||||
self._effective_low_priority_routes.extend(
|
||||
child_branch.effective_low_priority_routes()
|
||||
)
|
||||
self._effective_low_priority_routes_version = routes_version
|
||||
return self._effective_low_priority_routes
|
||||
with self._effective_routes_lock:
|
||||
routes_version = self.original_router._get_routes_version()
|
||||
if routes_version == self._effective_low_priority_routes_version:
|
||||
return self._effective_low_priority_routes
|
||||
effective_low_priority_routes: list[_EffectiveRouteContext] = []
|
||||
for route in self.original_router._low_priority_routes:
|
||||
route_context = self._build_effective_context(route)
|
||||
if route_context is not None:
|
||||
effective_low_priority_routes.append(route_context)
|
||||
for route in self.original_router.routes:
|
||||
if isinstance(route, _IncludedRouter):
|
||||
child_context = self.include_context.combine(route.include_context)
|
||||
child_branch = _IncludedRouter(
|
||||
original_router=route.original_router,
|
||||
include_context=child_context,
|
||||
)
|
||||
effective_low_priority_routes.extend(
|
||||
child_branch.effective_low_priority_routes()
|
||||
)
|
||||
self._effective_low_priority_routes = effective_low_priority_routes
|
||||
self._effective_low_priority_routes_version = routes_version
|
||||
return effective_low_priority_routes
|
||||
|
||||
def _build_effective_context(
|
||||
self, route: BaseRoute
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import threading
|
||||
from collections.abc import Sequence
|
||||
from typing import Annotated, cast
|
||||
|
||||
import pytest
|
||||
@@ -898,6 +900,66 @@ async def test_included_unknown_route_is_ignored_and_can_return_default_404():
|
||||
assert messages[0]["status"] == 404
|
||||
|
||||
|
||||
def test_included_router_candidate_cache_is_thread_safe():
|
||||
router = APIRouter()
|
||||
route_count = 120
|
||||
thread_count = 6
|
||||
for index in range(route_count):
|
||||
|
||||
@router.get(f"/items/{index}")
|
||||
def read_item(index: int = index):
|
||||
return {"index": index}
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
included_router = cast(_IncludedRouter, app.router.routes[-1])
|
||||
barrier = threading.Barrier(thread_count)
|
||||
results: list[Sequence[object]] = []
|
||||
|
||||
def build_candidates() -> None:
|
||||
barrier.wait()
|
||||
results.append(included_router.effective_candidates())
|
||||
|
||||
threads = [threading.Thread(target=build_candidates) for _ in range(thread_count)]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
assert len(results) == thread_count
|
||||
assert all(result is results[0] for result in results)
|
||||
assert len(results[0]) == route_count
|
||||
assert TestClient(app).get("/items/0").json() == {"index": 0}
|
||||
|
||||
|
||||
def test_included_router_low_priority_cache_rechecks_version_after_lock(monkeypatch):
|
||||
router = APIRouter()
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
included_router = cast(_IncludedRouter, app.router.routes[-1])
|
||||
routes_version = router._get_routes_version()
|
||||
version_checked = threading.Event()
|
||||
|
||||
def get_routes_version() -> int:
|
||||
version_checked.set()
|
||||
return routes_version
|
||||
|
||||
monkeypatch.setattr(router, "_get_routes_version", get_routes_version)
|
||||
result: list[Sequence[object]] = []
|
||||
|
||||
def build_low_priority_routes() -> None:
|
||||
result.append(included_router.effective_low_priority_routes())
|
||||
|
||||
with included_router._effective_routes_lock:
|
||||
thread = threading.Thread(target=build_low_priority_routes)
|
||||
thread.start()
|
||||
assert version_checked.wait(timeout=1)
|
||||
included_router._effective_low_priority_routes_version = routes_version
|
||||
thread.join()
|
||||
|
||||
assert result == [[]]
|
||||
|
||||
|
||||
def test_no_prefix_include_validation_sees_effective_starlette_route_candidates():
|
||||
def endpoint(request): # pragma: no cover
|
||||
return PlainTextResponse("ok")
|
||||
|
||||
Reference in new issue
Block a user