recipe: harden auth/metrics, add rate limiting and tests

This commit is contained in:
“Naeel”
2026-08-31 17:58:21 +03:00
parent 3d846d0be5
commit caeaa8c6fa
9 changed files with 452 additions and 71 deletions
+108
View File
@@ -641,3 +641,111 @@ nginx-маршруты без redirect и health `/receipt/health`. Старые
IP `127.0.0.1`, MIME `image/png`, размер 531101 байт, длину prompt 88 и
непустой usage JSON. Тестовый image-файл удалён после запроса; приложение
читает изображение в память и не пишет его на диск.
## 2026-08-31: Реализация backend-фиксов по плану v2
По команде пользователя выполнены изменения backend-компонентов и тестов.
### Изменения в recipe_service
- `recipe_service/metrics.py`:
- из `record()` удалён вызов `initialize()`;
- добавлена `count_since(client_ip, started_at_from)` для rate limiting.
- `recipe_service/app.py`:
- добавлен `ProxyFix(..., x_for=1, x_proto=1, x_host=1)`;
- сравнение токена переведено на `hmac.compare_digest`;
- введён единый финализатор `finalize(...)` вместо дублирования `record(...)`;
- `duration_ms` считается во всех ветках через `time.monotonic()`;
- добавлен rate limit `20` запросов/минута на IP (`429 too many requests`);
- зафиксирован контракт `502`:
`{"error":"upstream recognition failed","code":"upstream_error"}`;
- детали апстрима пишутся только в лог сервера с `request_id`.
- добавлен `recipe_service/test_app.py` (покрытие: `health`, `401`, `400`,
`415`, `413`, `200`, `502`-контракт, `429`).
### Изменения в gemini_proxy
- `gemini_proxy/app.py`: удалён `api_key_override`; ключ только из
`GEMINI_API_KEY`.
- `gemini_proxy/test_app.py`: добавлен тест, что `api_key_override` в форме
не даёт доступ без `GEMINI_API_KEY`.
### Изменения зависимостей
- выровнен root `requirements.txt` по version bounds с
`recipe_service/requirements.txt`:
- `Flask>=3.0,<4`
- `gunicorn>=21.2,<24`
- `requests>=2.31,<3`
### Проверки
- `py_compile` изменённых Python-файлов: успешно.
- `recipe_service`: `pytest -q` -> `8 passed`.
- `gemini_proxy`: `pytest -q` -> `4 passed`.
### Отдельно зафиксировано
Первый запуск тестов `recipe_service` дал `PermissionError` на `/var/lib/recipe`
при import-time `initialize()`. Исправлено в тесте ранней установкой
`RECIPE_METRICS_DB` в временный путь до импорта `app`.
## 2026-08-31: Безопасная оптимизация без смены поведения
По дополнительной команде пользователя выполнен пакет low-risk улучшений,
направленный на производительность и устойчивость, без изменения основного
контракта API.
### Изменения
- `recipe_service/metrics.py`:
- добавлен индекс
`idx_requests_client_ip_started_at ON requests(client_ip, started_at)`
для ускорения выборки rate limiting.
- `recipe_service/app.py`:
- ответ `429` унифицирован и дополнен стабильным полем
`code="rate_limited"` при сохранении `error="too many requests"`.
- `recipe_service/test_app.py`:
- обновлена проверка `429` с новым полем `code`;
- добавлен тест граничного случая лимитера (`19` запросов -> `200`);
- добавлен тест чтения последней записи в SQLite-метриках с проверкой
`status_code`, `duration_ms` и `error` после ветки `502`.
### Проверки
- `py_compile` изменённых Python-файлов: успешно.
- `recipe_service`: `pytest -q` -> `10 passed`.
- `gemini_proxy`: `pytest -q` -> `4 passed`.
### Вывод
Оптимизации применены без регрессий. Поведение успешного запроса, а также
статусы `400/401/413/415/502` сохранены; `429` дополнен машинным кодом
ошибки для стабильной клиентской обработки.
## 2026-08-31: Nginx-level rate limiting (основной лимитер)
По команде пользователя добавлен основной лимит запросов на уровне nginx,
при сохранении app-level fallback в `recipe_service/app.py`.
### Изменения конфигурации
- Добавлен новый файл `recipe_service/nginx-rate-limit-http.conf`:
- `limit_req_zone $binary_remote_addr zone=recipe_api_per_ip:10m rate=20r/m;`
- `limit_req_status 429;`
- файл предназначен для single-include внутри `http { ... }`.
- Обновлён `recipe_service/nginx-recipe.conf`:
- для `location = /recipe`, `location /recipe/`, `location = /receipt`,
`location /receipt/` добавлен
`limit_req zone=recipe_api_per_ip burst=5 nodelay;`.
### Результат
- Лимит теперь применяется единообразно для всех воркеров gunicorn на входе
nginx, а не только внутри отдельного процесса приложения.
- Python fallback-лимитер сохранён как защитный второй контур.
### Проверки
- `py_compile` изменённых Python-файлов: успешно.
- `recipe_service`: `pytest -q` -> `10 passed`.