pythonasyncioapiasyncfastapiframeworkjsonjson-schemaopenapiopenapi3pydanticpython-typespython3redocreststarletteswaggerswagger-uiuvicornweb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
6.3 KiB
237 lines
6.3 KiB
"""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.
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.routing import APIRoute
|
|
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_allow_header_includes_implicit_head_for_get_route():
|
|
app = FastAPI()
|
|
|
|
@app.get("/items")
|
|
def read_items():
|
|
return []
|
|
|
|
client = TestClient(app)
|
|
assert client.get("/items").json() == []
|
|
response = client.post("/items")
|
|
|
|
assert response.status_code == 405
|
|
assert set(response.headers["allow"].split(", ")) == {"GET", "HEAD"}
|
|
|
|
|
|
@pytest.mark.anyio
|
|
async def test_allow_header_response_without_app_scope_includes_implicit_head():
|
|
app = FastAPI()
|
|
|
|
@app.get("/items")
|
|
def read_items(): # pragma: no cover
|
|
return []
|
|
|
|
route = next(
|
|
route
|
|
for route in app.routes
|
|
if isinstance(route, APIRoute) and route.path == "/items"
|
|
)
|
|
messages = []
|
|
|
|
async def receive(): # pragma: no cover
|
|
return {"type": "http.request", "body": b"", "more_body": False}
|
|
|
|
async def send(message):
|
|
messages.append(message)
|
|
|
|
scope = {
|
|
"type": "http",
|
|
"method": "POST",
|
|
"path": "/items",
|
|
"raw_path": b"/items",
|
|
"root_path": "",
|
|
"scheme": "http",
|
|
"query_string": b"",
|
|
"headers": [],
|
|
}
|
|
|
|
await route.handle(scope, receive, send)
|
|
|
|
assert messages[0]["type"] == "http.response.start"
|
|
assert messages[0]["status"] == 405
|
|
assert set(dict(messages[0]["headers"])[b"allow"].decode().split(", ")) == {
|
|
"GET",
|
|
"HEAD",
|
|
}
|
|
|
|
|
|
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"]
|
|
|