FAPI005 route-shadowing¶
Co robi¶
Opt-in. Zgłasza operację ścieżki, do której nic nie dotrze, bo wcześniejsza odpowiada na każde żądanie, na jakie odpowiedziałaby ona:
- Przesłonięta:
GET /users/{id}jest zadeklarowane przedGET /users/me. Żądanie/users/mepasuje do pierwszej trasy, zidrównym"me", więc druga nigdy się nie wykona. - Powtórzona: ta sama metoda i ścieżka zadeklarowane dwa razy. FastAPI zostawia obie w schemacie OpenAPI, ale odpowiada tylko pierwsza.
Zgłoszenie stoi na dekoratorze nieosiągalnej operacji i wskazuje wcześniejszą wraz z plikiem i linią. Porównuje pełne ścieżki między routerami, które dołącza aplikacja, z prefiksami z APIRouter(prefix=...) i include_router(..., prefix=...).
Dlaczego to źle¶
FastAPI dopasowuje trasy w kolejności ich dodania, a wygrywa pierwsze dopasowanie, bez ostrzeżenia. Dokumentacja o tym mówi ("Order matters"). Agent, który dopisze GET /users/me pod GET /users/{id}, pisze handler, do którego nie trafi żadne żądanie, a test wołający funkcję handlera bezpośrednio przechodzi. Ruff sprawdza jeden plik naraz i nie ma na to reguły.
Przykład¶
[tool.inwards]
layers = [{ name = "app", modules = ["app"] }]
[tool.inwards.rules]
extend-select = ["FAPI005"]
from fastapi import FastAPI
from app import users
app = FastAPI()
app.include_router(users.router, prefix="/users")
from fastapi import APIRouter
router = APIRouter(tags=["users"])
@router.get("/{user_id}")
async def get_user(user_id: str) -> dict[str, str]:
return {"id": user_id}
@router.get("/me")
async def get_me() -> dict[str, str]:
return {"id": "me"}
app/users.py:11:1: FAPI005 `@router.get("/me")` (GET /users/me) can never be reached: `@router.get("/{user_id}")` (GET /users/{user_id}) at app/users.py:6 comes first and matches the same requests, so FastAPI always picks that one.
fix: Declare the specific route before the one with the path parameter.
1. Move `@router.get("/me")` (GET /users/me) above `@router.get("/{user_id}")` (GET /users/{user_id}), so the specific path is matched first.
2. If they are on different routers, include the router with the specific route first: the order of the include_router calls decides.
3. Don't delete either route to make this finding go away.
docs: https://sircypkowskyy.github.io/inwards/rules/FAPI005/
Poprawione: ścieżka dosłowna idzie pierwsza.
from fastapi import APIRouter
router = APIRouter(tags=["users"])
@router.get("/me")
async def get_me() -> dict[str, str]:
return {"id": "me"}
@router.get("/{user_id}")
async def get_user(user_id: str) -> dict[str, str]:
return {"id": user_id}
Jak działa¶
FAPI005 czyta wspólny model FastAPI (ADR-037) oraz graf aplikacji i routerów z FAPI003. Dla każdej aplikacji i dla każdego routera, którego nie dołącza żadna znana aplikacja ani router, układa trasy w kolejności, w jakiej trzyma je FastAPI: dekoratory i wywołania include_router aplikacji lub routera w kolejności w źródle, z trasami dołączonego routera w miejscu dołączenia, i z pełną ścieżką każdej trasy złożoną z prefiksów po drodze.
Trasa jest zgłaszana, gdy dla każdej metody, którą obsługuje, wcześniejsza trasa obsługuje tę metodę i pasuje do każdej ścieżki, do której pasuje ona. Ścieżki porównują się segment po segmencie, tak jak dopasowuje je Starlette:
{id}i{id:str}pasują do dowolnego niepustego segmentu: dosłownego albo parametru z dowolnym konwerterem pozapath.{id:int}pasuje do segmentu z cyfr, więc obejmuje/items/42i/items/{n:int}, ale nie/items/new.{rest:path}na końcu ścieżki pasuje do wszystkiego, co po niej następuje.- Nazwy parametrów nie mają znaczenia:
/u/{a}i/u/{b}to ta sama ścieżka, a druga jest zgłaszana jako powtórzenie. - Segment mieszający tekst z parametrem (
{name}.json) obejmuje tylko identyczny segment.
Metody porównują się osobno: @router.api_route("/a", methods=["GET", "POST"]) jest zgłaszane tylko wtedy, gdy nad nim obsługiwane są już i GET, i POST.
Gdzie działa:
inwards check,inwards baselinei Stop gate zgłaszają oba rodzaje, w obrębie routera i między routerami, w plikach, które sprawdzają.- Hook PostToolUse i edytor zgłaszają tylko to, co widać w jednym routerze w jednym pliku. Przesunięcie trasy w jednym pliku to edycja jednego pliku; podpięcie routerów do aplikacji to druga edycja, więc zgłoszenia między routerami czekają na Stop gate. Wyciszenie zgłoszenia między routerami nadal liczy się w hooku jako użyte.
Jak naprawić¶
- Przesłonięta: przenieś konkretną trasę nad tę z parametrem. Poprawka wymienia obie. Gdy leżą na różnych routerach, dołącz najpierw router z konkretną trasą.
- Powtórzona: zdecyduj, która z dwóch jest zamierzona, i nadaj drugiej własną ścieżkę lub metodę. Nie usuwaj pierwszej bez pytania: klienci trafiają do niej dziś.
Bezpieczeństwo poprawki¶
Brak automatycznej poprawki (autofix: false). Która trasa jest zamierzona i czy powtórzenie to pozostałość, czy pomyłka, to decyzje autora.
Konfiguracja¶
Waga: błąd. Opt-in: włącz przez extend-select. Nie ma własnych opcji; modules działa jak w każdej regule, a check-tests jak w każdej regule treści (tabele opcji).
Każde zgłoszenie stoi na dekoratorze i można je wyciszyć w kodzie przez # inwards: ignore[FAPI005] reason="..." w tej linii.
Znane ograniczenia¶
Nieznane oznacza ciszę. Nic nie jest zgłaszane, gdy:
- ścieżka, lista
methods=alboprefix=nie jest literałem tekstowym (zmienna, f-string, wywołanie) albo**kwargsmoże któreś ukrywać; - router trzyma trasy w kilku plikach, bo ich kolejność jest nieznana;
- segment używa konwertera, którego Inwards nie zna, innego niż identyczny;
- inne dołączenie tego samego routera, pod prefiksem, którego Inwards nie czyta, albo w innej aplikacji, zostawia trasę osiągalną;
- klasa tras routera może dopasowywać trasy po swojemu (#392): router Starlette uruchamia pierwszą trasę, której
matchesodpowiada pełnym dopasowaniem (routing.py,Router.app), więc własna klasa tras, która nadpisujematches,handlealbo__call__, może wybierać między trasami z tą samą metodą i ścieżką.VersionedAPIRoutePolara obsługuje dwie takie trasy według wersji API. Kolejność jest nieznana także wtedy, gdy klasy tras nie da się odczytać: router zwracany przez funkcję fabrykującą,route_class=, które nie jest nazwą, zewnętrzna klasa tras inna niż klasa FastAPI albo własna klasa routera, której klasy tras Inwards nie znajduje. Własna klasa routera z klasą tras FastAPI albo z klasą, która nie nadpisuje żadnej z tych metod (TransactionalAPIRoutew back office Polara), zachowuje odczytywaną kolejność.
Dalsze ograniczenia:
mountnie jest śledzony: zamontowana aplikacja routuje sama, po trasach aplikacji, na której jest zamontowana.- Trasy dodawane w czasie działania (
app.add_api_route(...),app.router.routes.insert(...)) nie są widoczne, podobnie jak trasy@router.websocket. - Trasy warunkowe (
if settings.DEBUG:) liczą się jako obecne. - Trasa zapisana pod
include_router, który dołącza jej router, w tym samym pliku, nie trafia do aplikacji, bo FastAPI kopiuje trasy istniejące w chwili wywołania (FAPI003 zgłasza to wywołanie). Jest pomijana w porównaniu. - Ukośniki końcowe:
/usersi/users/to tu różne ścieżki, tak jak w dopasowaniu Starlette przed jego przekierowaniem.
Źródła¶
- ADR-037: rodzina FastAPI, opt-in, i wspólny model.
- Dokumentacja FastAPI, Path Operation order: "Order matters".