Skip to content

fix: clarify gateway-backed chat auth errors - #3077

Merged
2 commits merged into
nesquena:masterfrom
AJV20:fix/gateway-chat-auth-diagnostics
May 28, 2026
Merged

2 commits merged into
nesquena:masterfrom
AJV20:fix/gateway-chat-auth-diagnostics

Conversation

@AJV20

@AJV20 AJV20 commented May 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Convert Gateway API Server HTTP 401s in gateway-backed browser chat into a specific gateway_auth_error that points operators at the WebUI↔Gateway API key mismatch.
  • Add redacted gateway_chat configuration status to /api/health/agent so support diagnostics can see whether gateway mode, base URL, and API-key presence are configured without exposing secret values.
  • Document the improved gateway-backed chat diagnostics in the README and changelog.

Test Plan

  • python -m py_compile api/gateway_chat.py api/routes.py
  • python -m pytest tests/test_webui_gateway_chat_backend.py tests/test_issue716_agent_heartbeat.py -q
  • git diff --check

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Read the full diff against origin/master and traced both ends: api/gateway_chat.py:79-114 for the new helpers, api/routes.py:3955-3961 for the health-route wiring, and the frontend apperror handler at static/messages.js:2088-2130 to confirm what actually reaches the user. Solid little PR — the redaction discipline is good, the test coverage is real, and the new _gateway_http_error_event() extraction makes the streaming worker easier to read. Two notes from the read-through.

The label drops back to "Error" because the frontend doesn't know about gateway_auth_error

The new apperror payload is:

return {
    "label": "Gateway authentication failed",
    "type": "gateway_auth_error",
    "message": "Gateway rejected the WebUI API key (HTTP 401).",
    "hint": (
        "Set HERMES_WEBUI_GATEWAY_API_KEY to the same value as the Hermes Gateway "
        "API_SERVER_KEY, or disable HERMES_WEBUI_CHAT_BACKEND=gateway."
        if not api_key_configured
        else "Check that HERMES_WEBUI_GATEWAY_API_KEY matches the Hermes Gateway API_SERVER_KEY."
    ),
}

The browser-side apperror handler at static/messages.js:2109-2114 only recognizes a fixed enum of type values and uses the literal label field nowhere:

const isRateLimit=d.type==='rate_limit';
const isQuotaExhausted=d.type==='quota_exhausted';
const isAuthMismatch=d.type==='auth_mismatch';
const isModelNotFound=d.type==='model_not_found';
…
const label=isCancelled?'Task cancelled':…:isAuthMismatch?(typeof t==='function'?t('provider_mismatch_label'):'Provider mismatch'):…:'Error';

So type: 'gateway_auth_error' doesn't match any of the special cases and falls through to the final 'Error' label. The hint will show up (lines 2115-2117 do render d.hint), so the user does see "Set HERMES_WEBUI_GATEWAY_API_KEY to the same value…" — that's the main win — but the headline says **Error:** Gateway rejected the WebUI API key (HTTP 401). instead of the carefully-worded Gateway authentication failed label sitting in the payload.

Two reasonable fixes, either is fine:

  1. Reuse the existing auth_mismatch type for HTTP 401 and add the gateway-specific hint there. The provider_mismatch_label i18n string at static/i18n.js:169 ("Provider mismatch") already gets pulled in 14 locales. This is the smaller, friendlier patch.

  2. Wire gateway_auth_error into the frontend dispatch: add const isGatewayAuth=d.type==='gateway_auth_error', then thread it into the label ternary. Probably also worth adding a gateway_auth_label i18n key for proper localization.

If you take option (1), the existing CLI-flavored hint at api/streaming.py:553 (Run \hermes model` in your terminal…`) is wrong for the gateway case, so you'd still want a branch on whether the failure came through the gateway path. Option (2) keeps the channels separate and is probably the right shape long-term.

gateway_chat payload is good, but no client reads it yet

/api/health/agent now includes:

payload["gateway_chat"] = gateway_chat_config_status()

gateway_chat_config_status() returns {enabled, backend, base_url_configured, api_key_configured} — all four are operator-safe (no secret values). I checked the browser-side consumer at static/ui.js:4656:

const payload=await api('/api/health/agent');
if(payload.alive === true){ … }
if(payload.alive === false){ _showAgentHealthAlert(payload); return; }
if(payload.alive == null){ _agentHealthLastState='unknown'; _hideAgentHealthAlert(); }

The new field is fine as a backend-only diagnostic surface — curl /api/health/agent | jq .gateway_chat is enough to know whether the operator has the env vars set without dumping secrets to logs. But the PR doesn't use it in the WebUI itself. If the intent was to display gateway config in the agent-down banner (_showAgentHealthAlert at ui.js:4640-4647), that wiring is missing. If the intent was operator support diagnostics only, then the README/CHANGELOG should be explicit about it being a /api/health/agent field, not user-facing UI.

Tests

tests/test_webui_gateway_chat_backend.py:38-65 covers gateway_chat_config_status correctly, including the important assertion assert "secret-token" not in repr(status). The 401 vs other-HTTP split test (test_gateway_http_401_reports_gateway_auth_not_provider_key) is the right shape. Worth adding one more test that ensures the resulting type value matches what the frontend dispatcher recognizes — that's exactly the kind of cross-layer contract test that would have surfaced the issue above.

Verdict

Land-able with a small follow-up. The 401-vs-other split and the redacted hint text are real improvements. Just decide whether gateway_auth_error is meant to be a real frontend-routed event type (then wire it in static/messages.js) or whether 401 should just feed into the existing auth_mismatch branch with a gateway-aware hint string.

@AJV20

AJV20 commented May 28, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in 923b719e.

What changed:

  • The browser apperror handler now recognizes gateway_auth_error before generic provider-auth handling.
  • Added the gateway_auth_label i18n key so the visible heading is Gateway authentication failed instead of Error.
  • Clarified that /api/health/agent.gateway_chat is an operator diagnostic payload, not a user-facing health banner yet.
  • Added regression coverage for the frontend label path, i18n key coverage, and diagnostic-only docs wording.

Verification:

  • python -m py_compile api/gateway_chat.py api/routes.py tests/test_webui_gateway_chat_backend.py
  • node --check static/messages.js
  • node --check static/i18n.js
  • pytest tests/test_webui_gateway_chat_backend.py -q --timeout=60 → 14 passed
  • git diff --check

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in 6267716 May 28, 2026
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Shipped in v0.51.154 / Release DZ (stage-batch36, commit 6267716). Thanks for the contribution!

ai-ag2026 pushed a commit to ai-ag2026/hermes-webui that referenced this pull request May 28, 2026
# Conflicts:
#	CHANGELOG.md
#	tests/test_webui_gateway_chat_backend.py
ai-ag2026 pushed a commit to ai-ag2026/hermes-webui that referenced this pull request May 28, 2026
9-PR medium-risk cleanup:
- nesquena#3037 routes.py: argv-style prefill hook + env-var override for notes drawer
- nesquena#3046 models.py: compression parent not repaired as stale interrupted turn
- nesquena#3048 session_discoverability.py: --repair-safe CLI with default dry-run
- nesquena#3053 ui.js: streaming KaTeX guard for parser-owned equations
- nesquena#3059 models.py: empty partial activity rows excluded from sidebar recency
- nesquena#3060 profiles.py: API key writes to .env (chmod 600), not config.yaml
- nesquena#3064 routes.py: MEDIA: image tokens allow exact session-referenced paths
- nesquena#3069 models.py: cron sessions with project_id surface via Cron Jobs chip
- nesquena#3077 gateway_chat.py: HTTP 401 maps to gateway_auth_error event
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
# Conflicts:
#	CHANGELOG.md
#	tests/test_webui_gateway_chat_backend.py
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
9-PR medium-risk cleanup:
- nesquena#3037 routes.py: argv-style prefill hook + env-var override for notes drawer
- nesquena#3046 models.py: compression parent not repaired as stale interrupted turn
- nesquena#3048 session_discoverability.py: --repair-safe CLI with default dry-run
- nesquena#3053 ui.js: streaming KaTeX guard for parser-owned equations
- nesquena#3059 models.py: empty partial activity rows excluded from sidebar recency
- nesquena#3060 profiles.py: API key writes to .env (chmod 600), not config.yaml
- nesquena#3064 routes.py: MEDIA: image tokens allow exact session-referenced paths
- nesquena#3069 models.py: cron sessions with project_id surface via Cron Jobs chip
- nesquena#3077 gateway_chat.py: HTTP 401 maps to gateway_auth_error event
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
# Conflicts:
#	CHANGELOG.md
#	tests/test_webui_gateway_chat_backend.py
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
9-PR medium-risk cleanup:
- nesquena#3037 routes.py: argv-style prefill hook + env-var override for notes drawer
- nesquena#3046 models.py: compression parent not repaired as stale interrupted turn
- nesquena#3048 session_discoverability.py: --repair-safe CLI with default dry-run
- nesquena#3053 ui.js: streaming KaTeX guard for parser-owned equations
- nesquena#3059 models.py: empty partial activity rows excluded from sidebar recency
- nesquena#3060 profiles.py: API key writes to .env (chmod 600), not config.yaml
- nesquena#3064 routes.py: MEDIA: image tokens allow exact session-referenced paths
- nesquena#3069 models.py: cron sessions with project_id surface via Cron Jobs chip
- nesquena#3077 gateway_chat.py: HTTP 401 maps to gateway_auth_error event
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants