fix(auth): логировать тело ответа при ошибке OAuth-обмена кода
All checks were successful
Deploy / test (push) Successful in 3m16s
Deploy / deploy (push) Successful in 17s

Живая проверка Google-входа упала с "401 Unauthorized" на /token, но лог
показывал только код статуса — тело ответа (там у Google/Яндекс error/
error_description с точной причиной: invalid_client и т.п.) терялось.

_raise_for_status_verbose() оборачивает raise_for_status(), добавляя resp.text
в сообщение исключения — на все 4 вызова (token+userinfo × google+yandex).
Чисто диагностическое изменение, поведение не меняет. Тесты/линт/mypy — ок.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-08-24 18:21:23 +05:00
parent 9f35ae8de1
commit c76f60df69

View File

@@ -20,6 +20,18 @@ class OAuthNotConfigured(Exception):
"""Провайдер не настроен (пустой client_id/secret в конфиге).""" """Провайдер не настроен (пустой client_id/secret в конфиге)."""
def _raise_for_status_verbose(resp: httpx.Response) -> None:
"""Как raise_for_status(), но с телом ответа в сообщении — у Google/Яндекс
там error/error_description с точной причиной (invalid_client и т.п.),
без этого в логе только код статуса, причина не видна."""
try:
resp.raise_for_status()
except httpx.HTTPStatusError as e:
raise httpx.HTTPStatusError(
f"{e}: {resp.text[:500]}", request=e.request, response=e.response
) from e
@dataclass @dataclass
class OAuthUserInfo: class OAuthUserInfo:
provider_id: str provider_id: str
@@ -86,14 +98,14 @@ async def exchange_code(provider: str, code: str) -> OAuthUserInfo:
"grant_type": "authorization_code", "grant_type": "authorization_code",
}, },
) )
token_resp.raise_for_status() _raise_for_status_verbose(token_resp)
access_token = token_resp.json()["access_token"] access_token = token_resp.json()["access_token"]
info_resp = await client.get( info_resp = await client.get(
"https://www.googleapis.com/oauth2/v3/userinfo", "https://www.googleapis.com/oauth2/v3/userinfo",
headers={"Authorization": f"Bearer {access_token}"}, headers={"Authorization": f"Bearer {access_token}"},
) )
info_resp.raise_for_status() _raise_for_status_verbose(info_resp)
info = info_resp.json() info = info_resp.json()
email = info["email"] email = info["email"]
return OAuthUserInfo( return OAuthUserInfo(
@@ -113,7 +125,7 @@ async def exchange_code(provider: str, code: str) -> OAuthUserInfo:
"client_secret": client_secret, "client_secret": client_secret,
}, },
) )
token_resp.raise_for_status() _raise_for_status_verbose(token_resp)
access_token = token_resp.json()["access_token"] access_token = token_resp.json()["access_token"]
info_resp = await client.get( info_resp = await client.get(
@@ -121,7 +133,7 @@ async def exchange_code(provider: str, code: str) -> OAuthUserInfo:
params={"format": "json"}, params={"format": "json"},
headers={"Authorization": f"OAuth {access_token}"}, headers={"Authorization": f"OAuth {access_token}"},
) )
info_resp.raise_for_status() _raise_for_status_verbose(info_resp)
info = info_resp.json() info = info_resp.json()
email = info.get("default_email") or next(iter(info.get("emails") or []), None) email = info.get("default_email") or next(iter(info.get("emails") or []), None)