Compare commits
No commits in common. "1a391caae28382abe1b1854505b52ec7af975c4f" and "85414dabd2af4bb1b7c11ec7a6ac8a694f29ae7e" have entirely different histories.
1a391caae2
...
85414dabd2
4 changed files with 1 additions and 95 deletions
|
|
@ -508,24 +508,3 @@ async def health() -> dict[str, str]:
|
||||||
"environment": settings.environment,
|
"environment": settings.environment,
|
||||||
"version": app.version,
|
"version": app.version,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# FastAPI/Starlette НЕ добавляет HEAD автоматически к @app.get() (в отличие от
|
|
||||||
# raw Starlette Route с methods=["GET"]) — без явного handler'а HEAD /health
|
|
||||||
# отдаёт 405. Это боевой прод-эндпоинт: Caddyfile:60 `handle /health {
|
|
||||||
# reverse_proxy backend:8000 }` — именно ЭТОТ хендлер отвечает на
|
|
||||||
# `HEAD https://gendsgn.ru/health`, которым бьёт внешний uptime-monitor
|
|
||||||
# (GlitchTip PING-тип шлёт HEAD, не GET) и не мог отличить "жив" от "мёртв" по
|
|
||||||
# статусу. media_type="application/json" — Content-Type совпадает с GET;
|
|
||||||
# Content-Length сознательно НЕ вычисляем под байт GET-ответа (пришлось бы
|
|
||||||
# дублировать сборку payload) — RFC 9110 §9.3.2 разрешает опускать payload-
|
|
||||||
# заголовки (Content-Length) для HEAD, требует совпадения только заголовков
|
|
||||||
# представления (Content-Type).
|
|
||||||
# include_in_schema=False: HEAD-проба — инфраструктура (uptime-monitor), а не часть
|
|
||||||
# контракта, по которому фронт генерирует типы. Без этого флага операция попадает в
|
|
||||||
# app.openapi(), и job `openapi-codegen-check` краснеет, требуя перегенерации
|
|
||||||
# frontend/src/types/api-types.ts — правки в сгенерированном файле ради маршрута,
|
|
||||||
# который фронт никогда не вызывает.
|
|
||||||
@app.head("/health", include_in_schema=False)
|
|
||||||
async def health_head() -> Response:
|
|
||||||
return Response(status_code=200, media_type="application/json")
|
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,3 @@ def test_health() -> None:
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
body = response.json()
|
body = response.json()
|
||||||
assert body["status"] == "ok"
|
assert body["status"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
def test_health_head_ok_no_body() -> None:
|
|
||||||
"""HEAD /health — то, что реально шлёт внешний uptime-monitor через Caddy
|
|
||||||
(`handle /health { reverse_proxy backend:8000 }`, Caddyfile:60), не GET.
|
|
||||||
|
|
||||||
Starlette не добавляет HEAD автоматически к `@app.get()` (в отличие от
|
|
||||||
низкоуровневого `Route(methods=["GET"])`) — без явного `@app.head()`
|
|
||||||
прод-эндпоинт отдаёт 405 на HEAD.
|
|
||||||
"""
|
|
||||||
client = TestClient(app)
|
|
||||||
response = client.head("/health")
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.content == b""
|
|
||||||
# RFC 9110 §9.3.2 — заголовки представления (Content-Type) должны совпадать
|
|
||||||
# с GET; Content-Length допустимо не совпадать (payload header field, MAY
|
|
||||||
# быть опущен для HEAD).
|
|
||||||
assert response.headers["content-type"] == "application/json"
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from collections.abc import AsyncGenerator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from fastapi import FastAPI, Response
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
||||||
from sentry_sdk.integrations.httpx import HttpxIntegration
|
from sentry_sdk.integrations.httpx import HttpxIntegration
|
||||||
|
|
@ -210,26 +210,6 @@ def health() -> dict[str, str]:
|
||||||
return {"status": "ok", "environment": settings.environment}
|
return {"status": "ok", "environment": settings.environment}
|
||||||
|
|
||||||
|
|
||||||
# FastAPI/Starlette НЕ добавляет HEAD автоматически к @app.get() (в отличие от
|
|
||||||
# raw Starlette Route с methods=["GET"]) — без явного handler'а HEAD /health
|
|
||||||
# отдаёт 405. NB: наружу через Caddy этот /health НЕ проксируется (только
|
|
||||||
# /trade-in/api/* → strip_prefix → tradein-backend:8000/api/v1/*), и никакой
|
|
||||||
# docker healthcheck на него сейчас тоже не настроен (grep по compose-файлам —
|
|
||||||
# только pg_isready для postgres) — маршрут пока используется лишь тестами.
|
|
||||||
# Внешний прод-симптом `HEAD gendsgn.ru/health -> 405` чинится в Site Finder
|
|
||||||
# (backend/app/main.py, за Caddyfile `handle /health`), не здесь.
|
|
||||||
# media_type="application/json" — Content-Type совпадает с GET; Content-Length
|
|
||||||
# сознательно НЕ вычисляем под байт GET-ответа (дублировало бы сборку payload)
|
|
||||||
# — RFC 9110 §9.3.2 разрешает опускать payload-заголовки (Content-Length) для
|
|
||||||
# HEAD, требует совпадения только заголовков представления (Content-Type).
|
|
||||||
# include_in_schema=False — по той же причине, что и у Site Finder: HEAD-проба это
|
|
||||||
# инфраструктура, а не контракт API. Здесь codegen-джоба пока нет, флаг ставим
|
|
||||||
# симметрично, чтобы схема двух бэкендов не разъезжалась.
|
|
||||||
@app.head("/health", include_in_schema=False)
|
|
||||||
def health_head() -> Response:
|
|
||||||
return Response(status_code=200, media_type="application/json")
|
|
||||||
|
|
||||||
|
|
||||||
app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"])
|
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(geocode.router, prefix="/api/v1/geocode", tags=["geocode"])
|
||||||
app.include_router(admin.router, prefix="/api/v1/admin", tags=["admin"])
|
app.include_router(admin.router, prefix="/api/v1/admin", tags=["admin"])
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
"""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""
|
|
||||||
# RFC 9110 §9.3.2 — HEAD должен вернуть те же заголовки представления
|
|
||||||
# (Content-Type), что и GET; Content-Length допустимо не совпадать (payload
|
|
||||||
# header field, MAY быть опущен для HEAD).
|
|
||||||
assert resp.headers["content-type"] == "application/json"
|
|
||||||
Loading…
Add table
Reference in a new issue