fix(auth): keep 401/403 Nous refresh replies terminal without an OAuth error code

The stack stopped defaulting an unknown non-200 token-endpoint body to
`invalid_grant` so a 429/404 gateway page no longer wipes credentials.
That also silently demoted 401/403 with a non-OAuth (or non-JSON) body
from terminal to "bench and retry every cooldown", so a dead grant on a
Portal that answers 401 without an `error` key would never prompt a
re-login. A 401/403 from the token endpoint always means the refresh
token itself was rejected, so mirror the Codex sibling
(auth_codex.py `status_code in {401, 403}` -> relogin) and keep the base
grant-dead default for exactly those two statuses; every other status
without an `error` key stays non-terminal.

Folding the non-JSON branch into the same path lets the `code`
coercion collapse to one line (gate quality finding).

Test: extend the kept parametrized row set with 401 (JSON, no `error`)
and 403 (non-JSON) rows asserting terminal; both fail on the previous
commit. Non-5xx rows no longer pin `retryable is None` (an unspecified
"raiser did not say" value), only the 5xx rows assert `retryable is True`.
This commit is contained in:
kshitijk4poor
2026-09-24 14:22:14 +05:30
committed by kshitij
parent 8a11d6342f
commit b8ce8875b0
2 changed files with 24 additions and 16 deletions

View File

@@ -616,12 +616,16 @@ def _refresh_access_token(
from hermes_cli.auth import _OAUTH_GRANT_DEAD_CODES
try:
error_payload = response.json()
except Exception as exc:
raise _nous_err("Refresh token exchange failed") from exc
except Exception:
error_payload = {}
# Only an explicit OAuth grant-dead code is terminal: a 429/404 gateway body without an
# ``error`` key says nothing about the refresh token, so it must not wipe credentials.
code = error_payload.get("error")
code = str(code) if code is not None else None
# A 401/403 from the token endpoint is the exception: it always means the refresh token
# itself was rejected (same rule as the Codex sibling), so keep the base grant-dead default.
raw_code = error_payload.get("error")
if raw_code is None and response.status_code in {401, 403}:
raw_code = "invalid_grant"
code = None if raw_code is None else str(raw_code)
description = str(error_payload.get("error_description") or "Refresh token exchange failed")
relogin = code in _OAUTH_GRANT_DEAD_CODES
# OAuth 2.1 "refresh token reuse": an external process (health check, monitoring tool, custom

View File

@@ -685,21 +685,24 @@ def test_refresh_token_reuse_detection_surfaces_actionable_message():
@pytest.mark.parametrize(
"status_code, body, expected_code, expected_retryable",
"status_code, body, expected_code, expected_terminal",
[
(500, None, "temporarily_unavailable", True),
(503, None, "temporarily_unavailable", True),
(599, None, "temporarily_unavailable", True),
(429, {"code": "429", "message": "rate limited"}, None, None),
(404, {"message": "not found"}, None, None),
(400, ValueError("not json"), None, None),
(500, None, "temporarily_unavailable", False),
(503, None, "temporarily_unavailable", False),
(599, None, "temporarily_unavailable", False),
(429, {"code": "429", "message": "rate limited"}, None, False),
(404, {"message": "not found"}, None, False),
(400, ValueError("not json"), None, False),
(401, {"message": "unauthorized"}, "invalid_grant", True),
(403, ValueError("not json"), "invalid_grant", True),
],
)
def test_refresh_token_exchange_without_grant_error_code_is_not_terminal(
status_code, body, expected_code, expected_retryable
status_code, body, expected_code, expected_terminal
):
"""A Portal 5xx is transient even when its body is not OAuth JSON (#120976), and a
non-5xx body that carries no OAuth ``error`` code must not be treated as a dead grant."""
non-5xx body that carries no OAuth ``error`` code must not be treated as a dead grant --
except a 401/403, which always means the refresh token itself was rejected."""
from hermes_cli.auth import _is_terminal_nous_refresh_error, _refresh_access_token
class _FakeResponse:
@@ -726,9 +729,10 @@ def test_refresh_token_exchange_without_grant_error_code_is_not_terminal(
)
assert exc_info.value.code == expected_code
assert exc_info.value.relogin_required is False
assert exc_info.value.retryable is expected_retryable
assert _is_terminal_nous_refresh_error(exc_info.value) is False
assert exc_info.value.relogin_required is expected_terminal
assert _is_terminal_nous_refresh_error(exc_info.value) is expected_terminal
if expected_code == "temporarily_unavailable":
assert exc_info.value.retryable is True
def test_runtime_refresh_503_preserves_nous_oauth_credentials(tmp_path, monkeypatch):