fix(grok): опрос device-flow не различал отказ, просрочку и ожидание
Владелец: «не даёт зайти в грок через аутх». Backend при этом исправен — проверено: провайдер возвращает настоящую сессию, адрес accounts.x.ai/oauth2/device и код. Дефект в цикле опроса: коды 400, 403 и 404 скопом считались «авторизация ещё не подтверждена» и опрос молча продолжался. Но в device-flow сервер сообщает РАЗНЫЕ вещи одним кодом 400, различая их полем error в теле: authorization_pending, slow_down, access_denied, expired_token. Следствие: отказ пользователя и просроченный код выглядели как ожидание. Мастер показывал «Ожидание подтверждения...» до самого таймаута и не говорил, что подтверждение уже отклонено или код давно истёк. Теперь каждый исход обрабатывается по существу: ожидание продолжает опрос, slow_down увеличивает интервал, отказ и просрочка прекращают его с внятным сообщением. Тесты: 414 passed, ruff чисто. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ad08c17d83
commit
50e4de5230
2 changed files with 84 additions and 3 deletions
|
|
@ -151,10 +151,39 @@ class GrokOAuthSession:
|
|||
self._finalize_with_tokens(access_token, refresh_token, poll_resp.get("id_token", ""))
|
||||
break
|
||||
except urllib.error.HTTPError as http_err:
|
||||
if http_err.code in (400, 403, 404):
|
||||
# Authorization pending
|
||||
# В device-flow сервер сообщает РАЗНЫЕ вещи одним кодом 400,
|
||||
# различая их полем error в теле. Раньше 400/403/404 скопом
|
||||
# считались «ещё не подтверждено», поэтому отказ пользователя и
|
||||
# просроченный код выглядели как ожидание: мастер крутил
|
||||
# «Ожидание подтверждения...» до таймаута и не говорил правду.
|
||||
reason = ""
|
||||
try:
|
||||
body = http_err.read().decode("utf-8", errors="replace")
|
||||
parsed = json.loads(body) if body else {}
|
||||
if isinstance(parsed, dict):
|
||||
reason = str(parsed.get("error") or "")
|
||||
except Exception:
|
||||
reason = ""
|
||||
|
||||
if reason == "authorization_pending" or (not reason and http_err.code in (400, 403, 404)):
|
||||
continue
|
||||
logger.warning("xAI device poll HTTP error: %d", http_err.code)
|
||||
if reason == "slow_down":
|
||||
self.interval = min(self.interval * 2, 30)
|
||||
continue
|
||||
if reason == "access_denied":
|
||||
self.status = "failed"
|
||||
self.error_msg = "Доступ отклонён: подтверждение в браузере не выдано"
|
||||
break
|
||||
if reason == "expired_token":
|
||||
self.status = "failed"
|
||||
self.error_msg = "Код устройства истёк, начните подключение заново"
|
||||
break
|
||||
|
||||
logger.warning("xAI device poll HTTP error: %d (%s)", http_err.code, reason or "без пояснения")
|
||||
if reason:
|
||||
self.status = "failed"
|
||||
self.error_msg = f"xAI отклонил запрос: {reason}"
|
||||
break
|
||||
except Exception as ex:
|
||||
logger.debug("xAI device poll error: %s", ex)
|
||||
|
||||
|
|
|
|||
52
tests/test_grok_device_flow.py
Normal file
52
tests/test_grok_device_flow.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
"""Опрос device-flow обязан различать исходы.
|
||||
|
||||
Владелец: «не даёт зайти в грок через аутх». Backend при этом исправен —
|
||||
провайдер возвращает настоящие адрес и код.
|
||||
|
||||
Дефект в цикле опроса: коды 400, 403 и 404 скопом считались «ещё не
|
||||
подтверждено». Но в device-flow сервер сообщает РАЗНЫЕ вещи одним кодом
|
||||
400, различая их полем error в теле: authorization_pending, slow_down,
|
||||
access_denied, expired_token. Отказ пользователя и просроченный код
|
||||
выглядели как ожидание — мастер крутил «Ожидание подтверждения...» до
|
||||
таймаута и не говорил правду.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import pathlib
|
||||
import urllib.error
|
||||
|
||||
|
||||
def _error(reason: str, code: int = 400) -> urllib.error.HTTPError:
|
||||
body = json.dumps({"error": reason}).encode("utf-8")
|
||||
return urllib.error.HTTPError("https://x", code, "Bad Request", {}, io.BytesIO(body))
|
||||
|
||||
|
||||
def test_poll_loop_handles_each_outcome_distinctly():
|
||||
src = pathlib.Path("src/antigravity_provider/router/grok_oauth.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "authorization_pending" in src, "ожидание больше не распознаётся явно"
|
||||
assert "access_denied" in src, "отказ пользователя не отличается от ожидания"
|
||||
assert "expired_token" in src, "просроченный код не отличается от ожидания"
|
||||
assert "slow_down" in src, "требование сбавить темп игнорируется"
|
||||
|
||||
# Прежняя конструкция считала все три кода одним исходом.
|
||||
assert "if http_err.code in (400, 403, 404):\n # Authorization pending" not in src
|
||||
|
||||
|
||||
def test_error_body_is_readable():
|
||||
"""Тело ответа должно разбираться — на нём и держится различение."""
|
||||
err = _error("access_denied")
|
||||
parsed = json.loads(err.read().decode("utf-8"))
|
||||
assert parsed["error"] == "access_denied"
|
||||
|
||||
|
||||
def test_denied_and_expired_are_terminal():
|
||||
"""Оба исхода обязаны прекращать опрос, а не продолжать его."""
|
||||
src = pathlib.Path("src/antigravity_provider/router/grok_oauth.py").read_text(encoding="utf-8")
|
||||
tail = src.split("if reason == \"access_denied\":", 1)[1][:400]
|
||||
assert "break" in tail, "отказ не прекращает опрос"
|
||||
expired = src.split("if reason == \"expired_token\":", 1)[1][:400]
|
||||
assert "break" in expired, "просрочка не прекращает опрос"
|
||||
Loading…
Reference in a new issue