{"record":{"id":"38b27606b180c81f","repo":"NousResearch/hermes-agent","slug":"api-auth-ws-ticket-http-res-status","errorCode":null,"errorMessage":"/api/auth/ws-ticket: HTTP ${res.status}","messagePattern":"/api/auth/ws-ticket: HTTP (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web/src/lib/api.ts","lineNumber":208,"sourceCode":"/**\n * Fetch a single-use ticket for a WebSocket upgrade in gated mode.\n *\n * The dashboard's gated-mode WS auth (``hermes_cli.web_server._ws_auth_ok``)\n * rejects the legacy ``?token=<_SESSION_TOKEN>`` path and only accepts\n * ``?ticket=<minted>`` consumed against the in-memory ticket store. Browsers\n * can't set ``Authorization`` on a WS upgrade, so this round-trip via the\n * authenticated REST endpoint is the bridge from cookie auth to WS auth.\n *\n * Tickets are single-use and TTL=30s — every WS connect attempt must\n * fetch a fresh ticket.\n */\nexport async function getWsTicket(): Promise<{ ticket: string; ttl_seconds: number }> {\n  const res = await fetch(`${BASE}/api/auth/ws-ticket`, {\n    method: \"POST\",\n    credentials: \"include\",\n  });\n  if (!res.ok) {\n    throw new Error(`/api/auth/ws-ticket: HTTP ${res.status}`);\n  }\n  return res.json();\n}\n\n/**\n * Resolve the auth query-param pair (``[name, value]``) for a WebSocket\n * connect. In gated mode mints a fresh single-use ticket; in loopback\n * mode returns the injected session token.\n */\nexport async function buildWsAuthParam(): Promise<[string, string]> {\n  if (window.__HERMES_AUTH_REQUIRED__) {\n    const { ticket } = await getWsTicket();\n    return [\"ticket\", ticket];\n  }\n  const token = window.__HERMES_SESSION_TOKEN__ ?? \"\";\n  return [\"token\", token];\n}\n","sourceCodeStart":190,"sourceCodeEnd":226,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/web/src/lib/api.ts#L190-L226","documentation":"In gated (auth-required) mode the dashboard bridges cookie auth to WebSocket auth by POSTing /api/auth/ws-ticket to mint a single-use, 30-second-TTL ticket. This error means that POST returned a non-OK status, so no ticket could be minted and the WS connect cannot proceed. The most common cause is an expired or missing session cookie.","triggerScenarios":"Calling getWsTicket() (directly or via buildWsAuthParam() → gatewayClient.connect()) after the dashboard session cookie expired, after gateway restart, or when the page was loaded without authentication. Also 403 if auth is required but the request lacks credentials.","commonSituations":"Dashboard tab left open overnight then attempting a reconnect; gateway process restarted clearing sessions; auth configuration toggled between loopback and gated mode while a tab was open; clock skew beyond ticket TTL logic.","solutions":["Reload the dashboard page and re-authenticate — this refreshes the cookie, then retry the WS connect (each attempt fetches a fresh ticket).","Check the gateway is up and its auth mode matches the page (window.__HERMES_AUTH_REQUIRED__ vs server config).","Ensure fetch sends credentials (credentials: 'include') if the dashboard is behind a proxy that strips cookies."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"async function connectWithFreshTicket(retries = 2): Promise<void> {\n  for (let attempt = 0; ; attempt++) {\n    try {\n      const { ticket } = await getWsTicket()\n      return await wsConnect(['ticket', ticket])\n    } catch (err) {\n      if (attempt >= retries || !String(err).includes('ws-ticket')) throw err\n      await sleep(500 * (attempt + 1))\n    }\n  }\n}","preventionTips":["Fetch a ticket immediately before every WS connect — never cache one (30s TTL, single use).","On 401 from the ticket endpoint, drive the page through a re-auth/reload before reconnecting.","Schedule reconnects with backoff instead of tight loops when the session lapses."],"tags":["websocket","auth","ticket","dashboard"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}