fix(backend): redact retryable MAX storage failures
CI / ruff · pytest (pull_request) Successful in 2m4s
CI / ruff · pytest (pull_request) Successful in 2m4s
Refs #82
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
await handle_update(event)
|
try:
|
||||||
|
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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user