{"record":{"id":"d819801dab7eb5a1","repo":"santifer/career-ops","slug":"gmail-token-refresh-returned-no-access-token","errorCode":null,"errorMessage":"Gmail token refresh returned no access_token","messagePattern":"Gmail token refresh returned no access_token","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/gmail/index.mjs","lineNumber":47,"sourceCode":"const STATE_PATH = 'data/gmail-state.json'; // the plugin's own processed-id cursor\n\n/** Exchange the long-lived refresh token for a short-lived access token. */\nasync function getAccessToken({ clientId, clientSecret, refreshToken }, fetchFn = globalThis.fetch) {\n  const res = await fetchFn(TOKEN_URL, {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n    body: new URLSearchParams({\n      client_id: clientId,\n      client_secret: clientSecret,\n      refresh_token: refreshToken,\n      grant_type: 'refresh_token',\n    }),\n  });\n  if (!res.ok) {\n    throw new Error(`Gmail token refresh failed: ${res.status} ${(await res.text()).slice(0, 200)}`);\n  }\n  const data = await res.json();\n  if (!data.access_token) throw new Error('Gmail token refresh returned no access_token');\n  return data.access_token;\n}\n\nfunction loadProcessedIds() {\n  if (!existsSync(STATE_PATH)) return new Set();\n  try {\n    const state = JSON.parse(readFileSync(STATE_PATH, 'utf-8'));\n    return new Set(state.processed_message_ids || []);\n  } catch {\n    return new Set();\n  }\n}\n\nfunction saveProcessedIds(ids) {\n  try {\n    mkdirSync('data', { recursive: true });\n    writeFileSync(STATE_PATH, JSON.stringify({ processed_message_ids: [...ids] }, null, 2), 'utf-8');\n  } catch (err) {","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/plugins/gmail/index.mjs#L29-L65","documentation":"Thrown by `getAccessToken` in the gmail plugin (plugins/gmail/index.mjs:47) when the token-refresh POST returns HTTP 200 OK but the JSON body has no `access_token` field. Google's token endpoint is expected to return `{ access_token, expires_in, ... }`; a 200 with no access_token is an unexpected/ malformed success response. This guard prevents returning `undefined` and passing it downstream as a bearer token.","triggerScenarios":"Google returns 200 but the JSON lacks `access_token` — e.g. an unexpected envelope, a response from an intercepting proxy, or an API anomaly. `if (!data.access_token)` fires after `res.json()`.","commonSituations":"A captive portal / corporate proxy returning a 200 HTML-or-JSON page that is not the real token response; a Google API transient anomaly; a misconfigured TOKEN_URL pointing at the wrong endpoint; response parsing returning an unexpected shape.","solutions":["Log `data` (without secrets) to see the actual response shape returned.","Confirm TOKEN_URL points at `https://oauth2.googleapis.com/token`.","If a proxy is intercepting, bypass it for oauth2.googleapis.com.","Retry once — if persistent and the body is clearly not a token response, investigate the network path."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Sanity-check the token endpoint response shape in a dry-run.\nasync function probeTokenEndpoint(url = TOKEN_URL) {\n  // A harmless OPTIONS probe to confirm the host is the real Google endpoint.\n  const res = await fetch(url, { method: 'OPTIONS' });\n  if (!res.ok && res.status !== 405) {\n    throw new Error(`Token endpoint ${url} looks intercepted (status ${res.status}).`);\n  }\n}\nawait probeTokenEndpoint();","typeGuard":"/** @param {unknown} data */\nfunction hasAccessToken(data) {\n  return data != null && typeof data.access_token === 'string' && data.access_token.length > 0;\n}","tryCatchPattern":"try {\n  const token = await getAccessToken(creds);\n} catch (err) {\n  if (/returned no access_token/.test(err.message)) {\n    console.error(`Unexpected token response — possible proxy interception: ${err.message}`);\n    // investigate the network path; do not coerce undefined into a bearer\n  } else throw err;\n}","preventionTips":["Bypass corporate/captive proxies for oauth2.googleapis.com.","Confirm TOKEN_URL points at https://oauth2.googleapis.com/token.","Log the response keys (never values) when the shape is unexpected."],"tags":["gmail","oauth","auth","response-shape","network"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}