fix(tradein): HEAD /health отвечает 200 вместо 405
@app.get("/health") в FastAPI/Starlette не добавляет HEAD-обработчик
автоматически (в отличие от низкоуровневого Route(methods=["GET"])) —
внешний uptime-monитор (GlitchTip PING-тип шлёт HEAD) получал 405 и не
мог отличить "жив" от "мёртв" по статусу. Добавлен явный
@app.head("/health") — 200 без тела (RFC 9110 §9.3.2), GET не тронут.
Тест test_health_endpoint.py фиксирует оба метода; RED до фикса
(HEAD → 405), GREEN после (проверено git stash + повторный прогон).
This commit is contained in:
parent
e8fe9faa13
commit
24b70e5c58
2 changed files with 43 additions and 1 deletions
|
|
@ -12,7 +12,7 @@ from collections.abc import AsyncGenerator
|
|||
from contextlib import asynccontextmanager
|
||||
|
||||
import sentry_sdk
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
||||
from sentry_sdk.integrations.httpx import HttpxIntegration
|
||||
|
|
@ -210,6 +210,17 @@ def health() -> dict[str, str]:
|
|||
return {"status": "ok", "environment": settings.environment}
|
||||
|
||||
|
||||
# FastAPI/Starlette НЕ добавляет HEAD автоматически к @app.get() (в отличие от
|
||||
# raw Starlette Route с methods=["GET"]) — без явного handler'а HEAD /health
|
||||
# отдаёт 405, и внешний uptime-monitor (GlitchTip PING-тип, HEAD-запрос) не
|
||||
# может отличить "жив" от "мёртв" по статусу. Тело для HEAD не отдаём — так
|
||||
# требует HTTP-спека (RFC 9110 §9.3.2): у ответа те же заголовки, что у GET,
|
||||
# но без body.
|
||||
@app.head("/health")
|
||||
def health_head() -> Response:
|
||||
return Response(status_code=200)
|
||||
|
||||
|
||||
app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"])
|
||||
app.include_router(geocode.router, prefix="/api/v1/geocode", tags=["geocode"])
|
||||
app.include_router(admin.router, prefix="/api/v1/admin", tags=["admin"])
|
||||
|
|
|
|||
31
tradein-mvp/backend/tests/test_health_endpoint.py
Normal file
31
tradein-mvp/backend/tests/test_health_endpoint.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
"""GET/HEAD /health — uptime-monitor honesty (#uptime-honest-green).
|
||||
|
||||
GlitchTip PING-мониторы шлют HEAD (или GET без чтения тела). Голый
|
||||
`@app.get("/health")` без явного HEAD-хендлера отдаёт 405 на HEAD — Starlette
|
||||
НЕ добавляет HEAD автоматически к FastAPI `@app.get()` роуту (в отличие от
|
||||
низкоуровневого `Route(methods=["GET"])`). Прод-симптом: `HEAD /health` → 405,
|
||||
монитор либо красный по конструкции, либо (при PING без сверки статуса)
|
||||
зелёный вне зависимости от факта. Тест фиксирует оба метода.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
|
||||
def test_health_get_ok() -> None:
|
||||
client = TestClient(app)
|
||||
resp = client.get("/health")
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["status"] == "ok"
|
||||
|
||||
|
||||
def test_health_head_ok_no_body() -> None:
|
||||
"""HEAD /health — то, что реально шлёт uptime-monitor. Должен быть 200, без тела."""
|
||||
client = TestClient(app)
|
||||
resp = client.head("/health")
|
||||
assert resp.status_code == 200
|
||||
assert resp.content == b""
|
||||
Loading…
Add table
Reference in a new issue