committed by
GitHub
2 changed files with 201 additions and 0 deletions
@ -0,0 +1,178 @@ |
|||||
|
"""Tests for automatic HEAD method support on GET routes. |
||||
|
|
||||
|
RFC 7231 §4.3.2 states that the server SHOULD send the same header fields |
||||
|
in response to a HEAD request as it would have sent if the request had been |
||||
|
a GET. FastAPI now automatically handles HEAD requests for all GET routes |
||||
|
without adding HEAD to the OpenAPI schema. |
||||
|
""" |
||||
|
|
||||
|
from fastapi import FastAPI |
||||
|
from fastapi.responses import JSONResponse |
||||
|
from fastapi.testclient import TestClient |
||||
|
from pydantic import BaseModel |
||||
|
|
||||
|
|
||||
|
def test_head_returns_200_for_get_route(): |
||||
|
app = FastAPI() |
||||
|
|
||||
|
@app.get("/") |
||||
|
def read_root(): |
||||
|
return {"hello": "world"} |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
response = client.head("/") |
||||
|
assert response.status_code == 200 |
||||
|
# HEAD response has no body but preserves headers |
||||
|
assert response.content == b"" |
||||
|
assert response.headers["content-length"] == "17" |
||||
|
assert response.headers["content-type"] == "application/json" |
||||
|
|
||||
|
|
||||
|
def test_head_works_with_path_params(): |
||||
|
app = FastAPI() |
||||
|
|
||||
|
@app.get("/items/{item_id}") |
||||
|
def read_item(item_id: int): |
||||
|
return {"item_id": item_id} |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
response = client.head("/items/42") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.content == b"" |
||||
|
|
||||
|
|
||||
|
def test_head_not_in_openapi_schema(): |
||||
|
app = FastAPI() |
||||
|
|
||||
|
@app.get("/") |
||||
|
def read_root(): |
||||
|
return {"hello": "world"} |
||||
|
|
||||
|
@app.get("/items/{item_id}") |
||||
|
def read_item(item_id: int): |
||||
|
return {"item_id": item_id} |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
assert client.get("/").json() == {"hello": "world"} |
||||
|
assert client.get("/items/42").json() == {"item_id": 42} |
||||
|
|
||||
|
schema = client.get("/openapi.json").json() |
||||
|
# HEAD should NOT appear in OpenAPI paths |
||||
|
assert list(schema["paths"]["/"].keys()) == ["get"] |
||||
|
assert list(schema["paths"]["/items/{item_id}"].keys()) == ["get"] |
||||
|
|
||||
|
|
||||
|
def test_head_not_added_to_non_get_routes(): |
||||
|
app = FastAPI() |
||||
|
|
||||
|
@app.post("/submit") |
||||
|
def submit(): |
||||
|
return {"ok": True} |
||||
|
|
||||
|
@app.put("/items/{item_id}") |
||||
|
def update_item(item_id: int): |
||||
|
return {"item_id": item_id} |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
assert client.post("/submit").json() == {"ok": True} |
||||
|
assert client.put("/items/1").json() == {"item_id": 1} |
||||
|
assert client.head("/submit").status_code == 405 |
||||
|
assert client.head("/items/1").status_code == 405 |
||||
|
|
||||
|
|
||||
|
def test_explicit_head_route_in_schema(): |
||||
|
"""When HEAD is explicitly declared, it SHOULD appear in OpenAPI.""" |
||||
|
app = FastAPI() |
||||
|
|
||||
|
@app.head("/health") |
||||
|
def health_check(): |
||||
|
return JSONResponse(None, headers={"x-status": "ok"}) |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
response = client.head("/health") |
||||
|
assert response.status_code == 200 |
||||
|
|
||||
|
schema = client.get("/openapi.json").json() |
||||
|
assert "head" in schema["paths"]["/health"] |
||||
|
|
||||
|
|
||||
|
def test_explicit_get_and_head_via_api_route(): |
||||
|
"""When GET and HEAD are both declared via api_route, both work.""" |
||||
|
app = FastAPI() |
||||
|
|
||||
|
@app.api_route("/both", methods=["GET", "HEAD"]) |
||||
|
def both_methods(): |
||||
|
return {"ok": True} |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
assert client.get("/both").status_code == 200 |
||||
|
assert client.head("/both").status_code == 200 |
||||
|
|
||||
|
|
||||
|
def test_get_still_works_after_auto_head(): |
||||
|
"""GET must not be affected by the auto HEAD feature.""" |
||||
|
app = FastAPI() |
||||
|
|
||||
|
@app.get("/data") |
||||
|
def get_data(): |
||||
|
return {"value": 123} |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
response = client.get("/data") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"value": 123} |
||||
|
|
||||
|
|
||||
|
def test_head_with_response_model(): |
||||
|
"""HEAD works correctly with routes that have response models.""" |
||||
|
app = FastAPI() |
||||
|
|
||||
|
class Item(BaseModel): |
||||
|
name: str |
||||
|
price: float |
||||
|
|
||||
|
@app.get("/item", response_model=Item) |
||||
|
def get_item(): |
||||
|
return Item(name="Widget", price=9.99) |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
response = client.head("/item") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.content == b"" |
||||
|
assert "content-length" in response.headers |
||||
|
|
||||
|
|
||||
|
def test_head_with_add_api_route(): |
||||
|
"""HEAD works for routes added via add_api_route().""" |
||||
|
app = FastAPI() |
||||
|
|
||||
|
def get_data(): |
||||
|
return {"data": True} |
||||
|
|
||||
|
app.add_api_route("/data", get_data) |
||||
|
client = TestClient(app) |
||||
|
response = client.head("/data") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.content == b"" |
||||
|
|
||||
|
|
||||
|
def test_head_with_router_include(): |
||||
|
"""HEAD works for routes added via APIRouter.""" |
||||
|
from fastapi import APIRouter |
||||
|
|
||||
|
app = FastAPI() |
||||
|
router = APIRouter() |
||||
|
|
||||
|
@router.get("/info") |
||||
|
def get_info(): |
||||
|
return {"info": "test"} |
||||
|
|
||||
|
app.include_router(router, prefix="/api") |
||||
|
client = TestClient(app) |
||||
|
response = client.head("/api/info") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.content == b"" |
||||
|
|
||||
|
# Not in OpenAPI schema |
||||
|
schema = client.get("/openapi.json").json() |
||||
|
assert list(schema["paths"]["/api/info"].keys()) == ["get"] |
||||
Loading…
Reference in new issue