NousResearch/hermes-agent · error

/api/auth/ws-ticket: HTTP ${res.status}

Error message

/api/auth/ws-ticket: HTTP ${res.status}

What it means

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.

Source

Thrown at web/src/lib/api.ts:208

/**
 * Fetch a single-use ticket for a WebSocket upgrade in gated mode.
 *
 * The dashboard's gated-mode WS auth (``hermes_cli.web_server._ws_auth_ok``)
 * rejects the legacy ``?token=<_SESSION_TOKEN>`` path and only accepts
 * ``?ticket=<minted>`` consumed against the in-memory ticket store. Browsers
 * can't set ``Authorization`` on a WS upgrade, so this round-trip via the
 * authenticated REST endpoint is the bridge from cookie auth to WS auth.
 *
 * Tickets are single-use and TTL=30s — every WS connect attempt must
 * fetch a fresh ticket.
 */
export async function getWsTicket(): Promise<{ ticket: string; ttl_seconds: number }> {
  const res = await fetch(`${BASE}/api/auth/ws-ticket`, {
    method: "POST",
    credentials: "include",
  });
  if (!res.ok) {
    throw new Error(`/api/auth/ws-ticket: HTTP ${res.status}`);
  }
  return res.json();
}

/**
 * Resolve the auth query-param pair (``[name, value]``) for a WebSocket
 * connect. In gated mode mints a fresh single-use ticket; in loopback
 * mode returns the injected session token.
 */
export async function buildWsAuthParam(): Promise<[string, string]> {
  if (window.__HERMES_AUTH_REQUIRED__) {
    const { ticket } = await getWsTicket();
    return ["ticket", ticket];
  }
  const token = window.__HERMES_SESSION_TOKEN__ ?? "";
  return ["token", token];
}

View on GitHub (pinned to c896c09c42)

Solutions

  1. Reload the dashboard page and re-authenticate — this refreshes the cookie, then retry the WS connect (each attempt fetches a fresh ticket).
  2. Check the gateway is up and its auth mode matches the page (window.__HERMES_AUTH_REQUIRED__ vs server config).
  3. Ensure fetch sends credentials (credentials: 'include') if the dashboard is behind a proxy that strips cookies.
Defensive patterns

Strategy: retry

Try / catch

async function connectWithFreshTicket(retries = 2): Promise<void> {
  for (let attempt = 0; ; attempt++) {
    try {
      const { ticket } = await getWsTicket()
      return await wsConnect(['ticket', ticket])
    } catch (err) {
      if (attempt >= retries || !String(err).includes('ws-ticket')) throw err
      await sleep(500 * (attempt + 1))
    }
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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