fix(openviking): match tenant-header errors structurally instead of hard-coding strings

The _needs_trusted_identity_retry method was hard-coding specific
server-side error strings to detect when a request failed due to
missing X-OpenViking-Account / X-OpenViking-User headers.  Each new
server-side error variant required another string added to the client.

Replace the string enumeration with a structural match: the error
message mentions one of the tenant headers AND the HTTP status is 400.
This covers all current error variants:

  - "Trusted mode requests must include X-OpenViking-Account and User"
  - "ROOT requests to tenant-scoped APIs must include X-OpenViking-Account"
  - "Trusted mode requests must include X-OpenViking-Account."
  - "Trusted mode requests must include X-OpenViking-User."

The 400 status guard avoids false-positives on 403 errors such as
"USER API keys cannot override X-OpenViking-User", which must not
trigger a retry.

All 176 existing tests pass.

(cherry picked from commit 5a24d6766c)
This commit is contained in:
爪爪 2026-07-06 15:16:31 +08:00 committed by kshitij
parent 82a383d970
commit 5ea3abc3b3
2 changed files with 24 additions and 9 deletions

View file

@ -294,12 +294,23 @@ class _VikingClient:
@staticmethod
def _needs_trusted_identity_retry(exc: Exception) -> bool:
"""Detect errors that indicate missing tenant-scoped identity headers.
Trusted mode can ask for ``X-OpenViking-Account`` /
``X-OpenViking-User`` using slightly different wording across
OpenViking versions. Match that trusted-mode missing-identity shape
instead of enumerating every exact string, while keeping deliberate
API-key permission denials non-retriable.
"""
message = str(exc)
return (
"Trusted mode requests must include X-OpenViking-Account" in message
or "Trusted mode requests must include X-OpenViking-User" in message
or "Trusted mode requests must include X-OpenViking-Account or explicit account_id" in message
)
if "Trusted mode requests must include" not in message:
return False
if "X-OpenViking-Account" not in message and "X-OpenViking-User" not in message:
return False
status_code = getattr(exc, "status_code", None)
if status_code is not None and status_code != 400:
return False
return True
def _send_with_trusted_identity_retry(self, send, *, multipart: bool = False) -> dict:
try:

View file

@ -1956,12 +1956,16 @@ def test_viking_client_does_not_retry_root_tenant_error_as_trusted_mode(monkeypa
captured_headers.append(kwargs.get("headers") or {})
if len(captured_headers) == 1:
return SimpleNamespace(
status_code=400,
status_code=403,
text="",
json=lambda: {
"error": {
"code": "INVALID_ARGUMENT",
"message": "ROOT requests to tenant-scoped APIs must include X-OpenViking-Account and X-OpenViking-User headers.",
"code": "PERMISSION_DENIED",
"message": (
"ROOT API keys cannot access tenant-scoped data APIs in api_key mode. "
"Use a user/admin API key for data access, or trusted mode for upstream "
"identity assertion."
),
},
},
raise_for_status=lambda: None,
@ -1975,7 +1979,7 @@ def test_viking_client_does_not_retry_root_tenant_error_as_trusted_mode(monkeypa
monkeypatch.setattr(client._httpx, "post", capture_post)
with pytest.raises(openviking_module._OpenVikingHTTPError, match="ROOT requests"):
with pytest.raises(openviking_module._OpenVikingHTTPError, match="ROOT API keys cannot access"):
client.post("/api/v1/search/search", {"query": "status"})
assert len(captured_headers) == 1