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

  1. Read probe.reason embedded in the message — it distinguishes handshake rejection (auth/origin) from network/timeout (proxy/firewall)
  2. Enable WebSocket proxying on the reverse proxy (nginx: proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_pass to /api/ws)
  3. Check the gateway's WS auth/origin guard config and allow the desktop client origin / ticket flow
  4. Ensure the URL scheme matches the transport (wss for TLS endpoints) and that no HTTP_1.0-style proxy sits in the path
  5. 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

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


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/439b8495600ac256. Report an issue: GitHub.