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
- 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.
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
- 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.
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
- 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
- ${res.status}: ${text}
- Session token not available — page must be served by the Her
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/38b27606b180c81f.
Report an issue: GitHub.