fix(backend): redact retryable MAX storage failures
CI / ruff · pytest (pull_request) Successful in 2m4s

Refs #82
This commit is contained in:
Pavel Eliseev
2026-10-04 16:04:38 +03:00
parent 64655ad934
commit 461c3d02ea
2 changed files with 17 additions and 1 deletions
+5
View File
@@ -10,6 +10,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
from app.config import get_settings from app.config import get_settings
from app.core import clock from app.core import clock
@@ -106,5 +107,9 @@ async def webhook(request: Request) -> dict[str, bool]:
raise HTTPException(422, "event must be an object") raise HTTPException(422, "event must be an object")
from app.channels.max_bot import handle_update from app.channels.max_bot import handle_update
try:
await handle_update(event) await handle_update(event)
except SQLAlchemyError:
# Let MAX retry; do not log DB exception parameters containing identity data.
raise HTTPException(503, "notification storage unavailable") from None
return {"success": True} return {"success": True}
+11
View File
@@ -1,9 +1,13 @@
"""MAX endpoints authenticate before reading bindings or accepting events.""" """MAX endpoints authenticate before reading bindings or accepting events."""
import sys
from types import SimpleNamespace
from unittest.mock import AsyncMock
from urllib.parse import parse_qs, urlsplit from urllib.parse import parse_qs, urlsplit
import pytest import pytest
from httpx import ASGITransport, AsyncClient from httpx import ASGITransport, AsyncClient
from sqlalchemy.exc import OperationalError
from app.config import get_settings from app.config import get_settings
from app.core import clock from app.core import clock
@@ -90,3 +94,10 @@ async def test_webhook_secret_checked_before_body_or_adapter(client):
assert (await client.post(HOOK, headers=headers, content="not-json")).status_code == 400 assert (await client.post(HOOK, headers=headers, content="not-json")).status_code == 400
assert (await client.post(HOOK, headers=headers, json=[])).status_code == 422 assert (await client.post(HOOK, headers=headers, json=[])).status_code == 422
assert (await client.post(HOOK, headers=headers, content=" " * 65537)).status_code == 413 assert (await client.post(HOOK, headers=headers, content=" " * 65537)).status_code == 413
async def test_webhook_storage_failure_is_retryable_without_identity_details(client, monkeypatch):
handler = AsyncMock(side_effect=OperationalError("private SQL", {"token": TOKEN}, Exception("private")))
monkeypatch.setitem(sys.modules, "app.channels.max_bot", SimpleNamespace(handle_update=handler))
response = await client.post(HOOK, headers={"X-Max-Bot-Api-Secret": SECRET}, json={})
assert response.status_code == 503 and TOKEN not in response.text and "private" not in response.text