{"record":{"id":"439b8495600ac256","repo":"NousResearch/hermes-agent","slug":"reached-the-gateway-over-http-but-the-live-websoc","errorCode":null,"errorMessage":"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.","messagePattern":"Reached the gateway over HTTP, but the live WebSocket \\(/api/ws\\) connection failed: (.+?) The HTTP check can pass while the WebSocket is blocked by a proxy, firewall, or gateway auth/origin guard\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/main.ts","lineNumber":7872,"sourceCode":"  const status = (await fetchJson(`${baseUrl}/api/status`, token, { timeoutMs: 8_000 })) as any\n\n  // The HTTP status check above proves the backend is reachable, but the chat\n  // surface only works once the renderer's live WebSocket to ``/api/ws``\n  // connects — a separate transport with separate server-side guards (Host/\n  // Origin, ws-ticket/token auth). Validating only the HTTP side produced a\n  // false-positive \"reachable\" while the real boot still failed with \"Could not\n  // connect to Hermes gateway\". Mirror the renderer's connect here so the test\n  // reflects the full path the app actually uses.\n  const wsUrl = await resolveTestWsUrl(baseUrl, authMode, token, { mintTicket: mintGatewayWsTicket })\n\n  // Skip the WS leg only when the runtime genuinely lacks a WebSocket (so an\n  // older Electron/Node never fails the test spuriously); Electron's main\n  // process ships a global WebSocket on every supported version.\n  if (wsUrl && typeof globalThis.WebSocket === 'function') {\n    const probe = await probeGatewayWebSocket(wsUrl, { WebSocketImpl: globalThis.WebSocket })\n\n    if (!probe.ok) {\n      throw new Error(\n        `Reached the gateway over HTTP, but the live WebSocket (/api/ws) connection failed: ${probe.reason} ` +\n          'The HTTP check can pass while the WebSocket is blocked by a proxy, firewall, or gateway auth/origin guard.'\n      )\n    }\n  }\n\n  return {\n    ok: true,\n    baseUrl,\n    version: status?.version || null\n  }\n}\n\nfunction resetBootProgressForReconnect() {\n  updateBootProgress(\n    {\n      error: null,\n      message: 'Restarting desktop connection',","sourceCodeStart":7854,"sourceCodeEnd":7890,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/main.ts#L7854-L7890","documentation":"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.","triggerScenarios":"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://.","commonSituations":"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.","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"],"exampleFix":"# before (nginx location block)\nlocation / { proxy_pass http://127.0.0.1:8080; }\n\n# after\nlocation / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection \"upgrade\"; }","handlingStrategy":"retry","validationCode":"// Pre-flight both legs before declaring a backend usable\nconst http = await fetch(`${baseUrl}/api/status`)\nif (!http.ok) throw new Error(`HTTP leg failed: ${http.status}`)\nconst ws = await probeGatewayWebSocket(wsUrl, { WebSocketImpl: globalThis.WebSocket })\nif (!ws.ok) throw new Error(`WS leg failed: ${ws.reason}`)","typeGuard":"function isWsProbeFailure(e: unknown): e is Error & { message: string } {\n  return e instanceof Error && e.message.startsWith('Reached the gateway over HTTP')\n}","tryCatchPattern":"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 }","preventionTips":["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"],"tags":["websocket","network","proxy","gateway","desktop","connectivity"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}