{"record":{"id":"c896a8c33e7c6412","repo":"NousResearch/hermes-agent","slug":"gateway-token-response-missing-access-token","errorCode":null,"errorMessage":"Gateway token response missing access_token","messagePattern":"Gateway token response missing access_token","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/desktop/electron/native-oauth.ts","lineNumber":182,"sourceCode":"  if (!expectedState || state !== expectedState) {\n    // Never redeem a code that arrived with a mismatched state — it may be a\n    // forged callback trying to inject an attacker's code.\n    throw new Error('Loopback callback state mismatch (possible CSRF)')\n  }\n\n  return { code }\n}\n\n/**\n * Normalize a `/auth/native/token` (or refresh) JSON response into a\n * NativeTokenSet, validating the shape. Throws on a missing/short access\n * token so a malformed response fails loudly rather than storing junk.\n */\nexport function parseTokenResponse(body: any): NativeTokenSet {\n  const accessToken = String(body?.access_token || '')\n\n  if (!accessToken) {\n    throw new Error('Gateway token response missing access_token')\n  }\n\n  const expiresAt = Number(body?.expires_at)\n\n  return {\n    accessToken,\n    refreshToken: String(body?.refresh_token || ''),\n    expiresAt: Number.isFinite(expiresAt) ? expiresAt : 0,\n    provider: String(body?.provider || ''),\n    userId: String(body?.user_id || '')\n  }\n}\n\n/**\n * Validate a token set loaded from the encrypted local store.\n *\n * The stored representation is already normalized as NativeTokenSet and\n * therefore uses camelCase. Gateway token responses use snake_case and","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/NousResearch/hermes-agent/blob/c896c09c42910c584c4c7d2325b58c14713ea42c/apps/desktop/electron/native-oauth.ts#L164-L200","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Log the raw response body — it usually contains the real reason (error field, HTML error page) the parser only sees as missing token","Update the hermes runtime/gateway to a version implementing /auth/native/token","Confirm the authorization code is fresh (single-use) and the flow wasn't retried with the same code","If a proxy sits in front of the gateway, bypass it to check whether it mangles the token response"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Sanity-check the exchange response before parsing\nconst res = await fetch(tokenEndpoint, { method: 'POST', body })\nconst body = await res.json().catch(() => null)\nif (!res.ok || !body?.access_token) throw new Error(`Token exchange failed (${res.status}): ${JSON.stringify(body)}`)\nconst tokens = parseTokenResponse(body)","typeGuard":"function isTokenResponseShape(body: unknown): body is { access_token: string } {\n  return typeof (body as any)?.access_token === 'string' && (body as any).access_token.length > 0\n}","tryCatchPattern":"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 }","preventionTips":["Check HTTP status and log the raw body before parsing the token response","Keep desktop app and hermes gateway versions aligned (native /auth/native/token support)","Never retry the exchange with an already-consumed authorization code"],"tags":["oauth","authentication","token","gateway","validation","desktop"],"backgroundTag":null,"analyzedSha":"c896c09c42910c584c4c7d2325b58c14713ea42c","analyzedAt":"2026-08-14T17:18:01.089Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}