fix(health): HEAD на /health в обоих бэкендах — аптайм проверял то, что всегда отвечает 405 #2893

Merged
lekss361 merged 3 commits from fix/tradein-uptime-honest-green into main 2026-08-15 16:40:16 +00:00
Owner

Проблема

Из аудита 15.08. За 72 часа аптайм показал 4292 успешные проверки из 4292 — при том, что снаружи HEAD https://gendsgn.ru/health отдавал 405, а HEAD /401.

То есть зелёный был получен не потому, что сервис жив, а потому, что проверка ничего не проверяла.

Что сделано

Добавлен обработчик HEAD на health-эндпоинт. Ответ 200 без тела, с тем же Content-Type: application/json, что и у GET — RFC 9110 требует у HEAD те же заголовки, что у GET.

Что поймало ревью

Фикс изначально лёг не в тот бэкенд. Наблюдаемый симптом — gendsgn.ru/health — обслуживает Site Finder: Caddyfile:60 роутит /health на backend:8000, а бэкенд «Меры» получает трафик только через /trade-in/api/*, и голый /health туда не попадает никогда. Правка же была сделана в «Мере».

Во втором круге маршрутизация проверена по Caddyfile в main, и обработчик добавлен в оба бэкенда: в Site Finder — чтобы починить наблюдаемые 405, в «Мере» — чтобы будущий монитор её собственного health не наступил на те же грабли.

Заодно эмпирически подтверждено расхождение заголовков: GET отдавал content-type: application/json, HEAD — вообще ничего. Исправлено.

Вердикт второго круга — MINOR.

Чего этот PR НЕ чинит

Корень ложного зелёного лежит внутри GlitchTip, а это чужой образ: для типа монитора PING код выставляет is_up=True до чтения ответа, а у всех пяти мониторов expected_status=None, то есть сравнивать не с чем. Патчить чужой сервис здесь не будем.

Лечится настройкой: перевести мониторы на GET и обязательно проставить expected_status=200. SQL подготовлен, но до выполнения требует правки — ревьюер справедливо указал, что целевые URL в нём заданы неверно: публичная страница «Меры» живёт на meraocenka.ru, а не на пути внутри gendsgn.ru. Плюс схему таблицы мониторов подтвердить не удалось (доступа к базе GlitchTip из рабочей сессии нет).

Поэтому SQL идёт отдельным шагом, вместе с настройкой получателей алертов — там же будут заведены мониторы «Меры», которых сейчас нет ни одного.

Test plan

  • тесты: HEAD /health возвращает 200 в обоих бэкендах, заголовки совпадают с GET
  • после деплоя: curl -I https://gendsgn.ru/health → 200 вместо 405
  • отдельно: правка мониторов в GlitchTip с корректными URL
## Проблема Из аудита 15.08. За 72 часа аптайм показал 4292 успешные проверки из 4292 — при том, что снаружи `HEAD https://gendsgn.ru/health` отдавал **405**, а `HEAD /` — **401**. То есть зелёный был получен не потому, что сервис жив, а потому, что проверка ничего не проверяла. ## Что сделано Добавлен обработчик `HEAD` на health-эндпоинт. Ответ 200 без тела, с тем же `Content-Type: application/json`, что и у `GET` — RFC 9110 требует у HEAD те же заголовки, что у GET. ## Что поймало ревью **Фикс изначально лёг не в тот бэкенд.** Наблюдаемый симптом — `gendsgn.ru/health` — обслуживает Site Finder: `Caddyfile:60` роутит `/health` на `backend:8000`, а бэкенд «Меры» получает трафик только через `/trade-in/api/*`, и голый `/health` туда не попадает никогда. Правка же была сделана в «Мере». Во втором круге маршрутизация проверена по `Caddyfile` в `main`, и обработчик добавлен **в оба** бэкенда: в Site Finder — чтобы починить наблюдаемые 405, в «Мере» — чтобы будущий монитор её собственного health не наступил на те же грабли. Заодно эмпирически подтверждено расхождение заголовков: `GET` отдавал `content-type: application/json`, `HEAD` — вообще ничего. Исправлено. Вердикт второго круга — **MINOR**. ## Чего этот PR НЕ чинит Корень ложного зелёного лежит **внутри GlitchTip**, а это чужой образ: для типа монитора PING код выставляет `is_up=True` до чтения ответа, а у всех пяти мониторов `expected_status=None`, то есть сравнивать не с чем. Патчить чужой сервис здесь не будем. Лечится настройкой: перевести мониторы на GET и обязательно проставить `expected_status=200`. SQL подготовлен, но **до выполнения требует правки** — ревьюер справедливо указал, что целевые URL в нём заданы неверно: публичная страница «Меры» живёт на `meraocenka.ru`, а не на пути внутри `gendsgn.ru`. Плюс схему таблицы мониторов подтвердить не удалось (доступа к базе GlitchTip из рабочей сессии нет). Поэтому SQL идёт отдельным шагом, вместе с настройкой получателей алертов — там же будут заведены мониторы «Меры», которых сейчас нет ни одного. ## Test plan - [x] тесты: `HEAD /health` возвращает 200 в обоих бэкендах, заголовки совпадают с `GET` - [ ] после деплоя: `curl -I https://gendsgn.ru/health` → 200 вместо 405 - [ ] отдельно: правка мониторов в GlitchTip с корректными URL
lekss361 added 2 commits 2026-08-15 16:02:36 +00:00
@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 + повторный прогон).
fix(health): HEAD /health на верном бэкенде (Site Finder) + честные заголовки
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Failing after 2m34s
CI Trade-In / backend-tests (pull_request) Successful in 5m4s
CI / backend-tests (pull_request) Successful in 16m49s
3f5f099392
Review-разбор ветки fix/tradein-uptime-honest-green:

1. [HIGH] Прод-симптом `HEAD gendsgn.ru/health -> 405` обслуживает Site
   Finder (Caddyfile:60 `handle /health { reverse_proxy backend:8000 }`),
   а предыдущий коммит правил только tradein-mvp/backend, чей /health наружу
   не проксируется вообще. Добавлен @app.head("/health") в backend/app/main.py
   рядом с существующим @app.get — эмпирически подтверждено (uv run pytest):
   HEAD было 405, стало 200. tradein-mvp фикс не откачен (безвреден, годится
   для будущего internal-caller), но обвязан комментарием, что реальный
   прод-путь чинится не там.

2. [LOW] Response(status_code=200) без media_type отдавал HEAD без
   Content-Type, тогда как GET отдаёт application/json — расходится с
   заявленным в комментарии RFC 9110 §9.3.2. Добавлен media_type в обоих
   бэкендах; Content-Length сознательно не подгоняем под байты GET-ответа
   (payload header field, RFC разрешает опускать для HEAD) — не дублируем
   сборку payload ради байт-в-байт соответствия.

Тесты: test_health_head_ok_no_body добавлен в backend/tests/test_health.py
(Site Finder) — RED-check (git stash app/main.py) воспроизводит прод-баг
1:1: assert 405 == 200. tradein-mvp/backend/tests/test_health_endpoint.py
дополнен проверкой Content-Type. uv run pytest — все зелёные.
bot-backend added 1 commit 2026-08-15 16:22:28 +00:00
fix(health): не тащить HEAD-пробу в OpenAPI-схему
All checks were successful
CI Trade-In / changes (pull_request) Successful in 17s
CI / changes (pull_request) Successful in 16s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m53s
CI Trade-In / backend-tests (pull_request) Successful in 5m16s
CI / backend-tests (pull_request) Successful in 17m1s
cb0f42d1b1
Джоба openapi-codegen-check покраснела на этой ветке: она дампит app.openapi(),
регенерирует frontend/src/types/api-types.ts и падает на расхождении. Добавленный
HEAD /health попал в схему и потребовал правки сгенерированного файла.

Регенерировать типы ради маршрута, который фронт никогда не вызывает, — лишний
шум в generated-коде. HEAD-проба это инфраструктура для uptime-монитора, а не
часть контракта, по которому фронт строит типы, поэтому include_in_schema=False
здесь и по смыслу верно, а не только удобно.

Флаг ставим в обоих бэкендах симметрично: у trade-in codegen-джобы пока нет, но
расхождение схем между двумя бэкендами потом само станет источником вопросов.
lekss361 merged commit 1a391caae2 into main 2026-08-15 16:40:16 +00:00
lekss361 deleted branch fix/tradein-uptime-honest-green 2026-08-15 16:40:18 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2893
No description provided.