NousResearch/hermes-agent · error · Error

Gateway token response missing access_token

Error message

Gateway token response missing access_token

What it means

parseTokenResponse normalizes the JSON from the gateway's /auth/native/token (or refresh) endpoint into a NativeTokenSet, validating the shape so a malformed response fails loudly instead of storing junk credentials. A missing/empty access_token in the body throws this error. It means the token exchange endpoint answered 200-ish JSON without a usable token — typically a gateway version that doesn't implement the native token endpoint, or an error payload being parsed as success.

Source

Thrown at apps/desktop/electron/native-oauth.ts:182

  if (!expectedState || state !== expectedState) {
    // Never redeem a code that arrived with a mismatched state — it may be a
    // forged callback trying to inject an attacker's code.
    throw new Error('Loopback callback state mismatch (possible CSRF)')
  }

  return { code }
}

/**
 * Normalize a `/auth/native/token` (or refresh) JSON response into a
 * NativeTokenSet, validating the shape. Throws on a missing/short access
 * token so a malformed response fails loudly rather than storing junk.
 */
export function parseTokenResponse(body: any): NativeTokenSet {
  const accessToken = String(body?.access_token || '')

  if (!accessToken) {
    throw new Error('Gateway token response missing access_token')
  }

  const expiresAt = Number(body?.expires_at)

  return {
    accessToken,
    refreshToken: String(body?.refresh_token || ''),
    expiresAt: Number.isFinite(expiresAt) ? expiresAt : 0,
    provider: String(body?.provider || ''),
    userId: String(body?.user_id || '')
  }
}

/**
 * Validate a token set loaded from the encrypted local store.
 *
 * The stored representation is already normalized as NativeTokenSet and
 * therefore uses camelCase. Gateway token responses use snake_case and

View on GitHub (pinned to c896c09c42)

Solutions

  1. Log the raw response body — it usually contains the real reason (error field, HTML error page) the parser only sees as missing token
  2. Update the hermes runtime/gateway to a version implementing /auth/native/token
  3. Confirm the authorization code is fresh (single-use) and the flow wasn't retried with the same code
  4. If a proxy sits in front of the gateway, bypass it to check whether it mangles the token response
Defensive patterns

Strategy: try-catch

Validate before calling

// Sanity-check the exchange response before parsing
const res = await fetch(tokenEndpoint, { method: 'POST', body })
const body = await res.json().catch(() => null)
if (!res.ok || !body?.access_token) throw new Error(`Token exchange failed (${res.status}): ${JSON.stringify(body)}`)
const tokens = parseTokenResponse(body)

Type guard

function isTokenResponseShape(body: unknown): body is { access_token: string } {
  return typeof (body as any)?.access_token === 'string' && (body as any).access_token.length > 0
}

Try / catch

try { const tokens = parseTokenResponse(body) } catch (e) { if (e instanceof Error && e.message === 'Gateway token response missing access_token') { logRawBody(body); checkGatewayVersion(); throw new Error('Gateway did not return a native token — update the hermes runtime?') } throw e }

Prevention

When it happens

Trigger: POST /auth/native/token (or /auth/native/refresh) returns JSON lacking access_token — older gateway without native-flow support, an error envelope {error: ...} with HTTP 200, a proxy rewriting the response, or a code already redeemed/expired so the gateway returns an error body.

Common situations: Desktop app newer than the deployed hermes runtime (native flow absent); gateway behind an API gateway that swallows the token response; retrying the exchange with a consumed authorization code.

Related errors


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