Browse Source

fix: include implicit HEAD in allow header

pull/15356/head
Herrtian 1 month ago
parent
commit
65bf29d55a
  1. 21
      fastapi/routing.py
  2. 14
      tests/test_auto_head_for_get.py
  3. 7
      tests/test_router_include_context.py

21
fastapi/routing.py

@ -1166,6 +1166,12 @@ class APIRoute(routing.Route):
and "HEAD" not in route_methods
)
def _allow_methods(self, methods: Collection[str]) -> set[str]:
allow_methods = set(methods)
if "GET" in allow_methods and "HEAD" not in allow_methods:
allow_methods.add("HEAD")
return allow_methods
def matches(self, scope: Scope) -> tuple[Match, Scope]:
effective_context = _get_scope_effective_route_context(scope)
if effective_context is not None and effective_context.original_route is self:
@ -1191,7 +1197,7 @@ class APIRoute(routing.Route):
and scope["method"] not in methods
and not self._is_head_for_get(scope, methods)
):
headers = {"Allow": ", ".join(methods)}
headers = {"Allow": ", ".join(self._allow_methods(methods))}
if "app" in scope:
raise HTTPException(status_code=405, headers=headers)
response = PlainTextResponse(
@ -1206,6 +1212,19 @@ class APIRoute(routing.Route):
_effective_route_context_var.reset(token)
await app(scope, receive, send)
return
if (
self.methods
and scope["method"] not in self.methods
and not self._is_head_for_get(scope)
):
headers = {"Allow": ", ".join(self._allow_methods(self.methods))}
if "app" in scope:
raise HTTPException(status_code=405, headers=headers)
response = PlainTextResponse(
"Method Not Allowed", status_code=405, headers=headers
)
await response(scope, receive, send)
return
# Allow HEAD requests through to the GET handler.
if self._is_head_for_get(scope):
await self.app(scope, receive, send)

14
tests/test_auto_head_for_get.py

@ -62,6 +62,20 @@ def test_head_not_in_openapi_schema():
assert list(schema["paths"]["/items/{item_id}"].keys()) == ["get"]
def test_allow_header_includes_implicit_head_for_get_route():
app = FastAPI()
@app.get("/items")
def read_items():
return []
client = TestClient(app)
response = client.post("/items")
assert response.status_code == 405
assert set(response.headers["allow"].split(", ")) == {"GET", "HEAD"}
def test_head_not_added_to_non_get_routes():
app = FastAPI()

7
tests/test_router_include_context.py

@ -532,7 +532,7 @@ def test_included_partial_match_returns_405_when_no_later_full_match_exists():
response = TestClient(app).post("/api/items")
assert response.status_code == 405
assert response.headers["allow"] == "GET"
assert set(response.headers["allow"].split(", ")) == {"GET", "HEAD"}
def test_included_slash_redirect_does_not_block_later_exact_match():
@ -682,7 +682,10 @@ async def test_included_api_route_without_app_scope_returns_405_response():
assert messages[0]["type"] == "http.response.start"
assert messages[0]["status"] == 405
assert dict(messages[0]["headers"])[b"allow"] == b"GET"
assert set(dict(messages[0]["headers"])[b"allow"].decode().split(", ")) == {
"GET",
"HEAD",
}
def test_effective_api_route_context_does_not_match_websocket_scope():

Loading…
Cancel
Save