NousResearch/hermes-agent · error · Error
Reached the gateway over HTTP, but the live WebSocket (/api/
Error message
Reached the gateway over HTTP, but the live WebSocket (/api/ws) connection failed: ${probe.reason} The HTTP check can pass while the WebSocket is blocked by a proxy, firewall, or gateway auth/origin guard. What it means
The gateway connectivity test reached the Hermes backend over HTTP (GET /api/status passed) but the follow-up live WebSocket probe to /api/ws failed. The test intentionally mirrors the renderer's real connection path, so an HTTP-only success is treated as a failure. Typical causes are intermediaries (proxy, firewall) that allow plain HTTP but block or corrupt WebSocket upgrades, or gateway-side auth/origin guards that reject the WS handshake specifically.
Source
Thrown at apps/desktop/electron/main.ts:7872
const status = (await fetchJson(`${baseUrl}/api/status`, token, { timeoutMs: 8_000 })) as any
// The HTTP status check above proves the backend is reachable, but the chat
// surface only works once the renderer's live WebSocket to ``/api/ws``
// connects — a separate transport with separate server-side guards (Host/
// Origin, ws-ticket/token auth). Validating only the HTTP side produced a
// false-positive "reachable" while the real boot still failed with "Could not
// connect to Hermes gateway". Mirror the renderer's connect here so the test
// reflects the full path the app actually uses.
const wsUrl = await resolveTestWsUrl(baseUrl, authMode, token, { mintTicket: mintGatewayWsTicket })
// Skip the WS leg only when the runtime genuinely lacks a WebSocket (so an
// older Electron/Node never fails the test spuriously); Electron's main
// process ships a global WebSocket on every supported version.
if (wsUrl && typeof globalThis.WebSocket === 'function') {
const probe = await probeGatewayWebSocket(wsUrl, { WebSocketImpl: globalThis.WebSocket })
if (!probe.ok) {
throw new Error(
`Reached the gateway over HTTP, but the live WebSocket (/api/ws) connection failed: ${probe.reason} ` +
'The HTTP check can pass while the WebSocket is blocked by a proxy, firewall, or gateway auth/origin guard.'
)
}
}
return {
ok: true,
baseUrl,
version: status?.version || null
}
}
function resetBootProgressForReconnect() {
updateBootProgress(
{
error: null,
message: 'Restarting desktop connection',View on GitHub (pinned to c896c09c42)
Solutions
- Read probe.reason embedded in the message — it distinguishes handshake rejection (auth/origin) from network/timeout (proxy/firewall)
- Enable WebSocket proxying on the reverse proxy (nginx: proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_pass to /api/ws)
- Check the gateway's WS auth/origin guard config and allow the desktop client origin / ticket flow
- Ensure the URL scheme matches the transport (wss for TLS endpoints) and that no HTTP_1.0-style proxy sits in the path
- Temporarily connect the desktop app directly to the gateway port to isolate the intermediary as the cause
Example fix
# before (nginx location block)
location / { proxy_pass http://127.0.0.1:8080; }
# after
location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } Defensive patterns
Strategy: retry
Validate before calling
// Pre-flight both legs before declaring a backend usable
const http = await fetch(`${baseUrl}/api/status`)
if (!http.ok) throw new Error(`HTTP leg failed: ${http.status}`)
const ws = await probeGatewayWebSocket(wsUrl, { WebSocketImpl: globalThis.WebSocket })
if (!ws.ok) throw new Error(`WS leg failed: ${ws.reason}`) Type guard
function isWsProbeFailure(e: unknown): e is Error & { message: string } {
return e instanceof Error && e.message.startsWith('Reached the gateway over HTTP')
} Try / catch
try { await testConnection(baseUrl) } catch (e) { if (isWsProbeFailure(e)) { reportToUser(`WebSocket blocked (${extractReason(e.message)}). Check proxy Upgrade headers / gateway origin guard.`); await suggestDirectConnection() } else throw e } Prevention
- Always configure WebSocket upgrade headers on reverse proxies in front of the gateway
- Include a WS probe in deployment health checks, not just HTTP GET /api/status
- Keep gateway origin/auth allow-lists in sync with desktop client origins
- Match wss:// scheme to TLS endpoints
When it happens
Trigger: Calling the connection-test IPC with a baseUrl behind a reverse proxy (nginx/nginx-ingress without Upgrade headers), a corporate firewall that strips Connection: Upgrade, a gateway auth/origin guard rejecting the WS origin or a bad/mint-failed ticket from resolveTestWsUrl, or the WS endpoint requiring wss:// while the probe used ws://.
Common situations: Self-hosting the Hermes gateway behind nginx/Traefik without proxy_set_header Upgrade/Connection; TLS-terminating proxies where the app computes ws:// against an https URL; gateway deployments with an origin allow-list that does not include the desktop app origin; auth proxies that pass REST but challenge WS handshakes.
Related errors
- Gateway did not return a WS ticket.
- Hermes backend for profile "${profile}" is HTTP-reachable bu
- Local Hermes backend is HTTP-reachable but the WebSocket (/a
- Remote gateway session token is required.
- Remote Hermes gateway is selected, but no session token is s
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/439b8495600ac256.
Report an issue: GitHub.