{"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/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/plugins/gmail/index.mjs#L29-L65","documentation":"After a successful (2xx) token refresh response, getAccessToken parses the JSON and requires an access_token field. If Google's response lacks it despite being ok, the library throws this error since it cannot authenticate subsequent Gmail API calls.","triggerScenarios":"Google returns 200 with a JSON body that has no access_token — unexpected API response, proxy interception, or a non-token JSON payload (e.g. an HTML-captured login page served with 200).","commonSituations":"Corporate proxy/captive portal returning a 200 HTML page; grant_type silently ignored due to a misconfigured request; Google API behavior change; response body actually an error description with a 200 status from a man-in-the-middle.","solutions":["Log the raw response body (res.text()) to see what was actually returned.","Bypass any corporate proxy/captive portal and retry.","Verify the token endpoint URL is exactly https://oauth2.googleapis.com/token over HTTPS.","Retry — if transient, a second refresh typically returns a valid access_token."],"exampleFix":"// before\nconst data = await res.json();\nif (!data.access_token) throw new Error('Gmail token refresh returned no access_token');\n// after\nconst data = await res.json();\nif (!data.access_token) {\n  console.error('unexpected token response keys:', Object.keys(data));\n  throw new Error('Gmail token refresh returned no access_token');\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const token = await getAccessToken();\n} catch (e) {\n  if (String(e.message).includes('no access_token')) {\n    // retry once; if persistent, log raw body and bypass proxy\n    return retryWithBackoff(getAccessToken, 2);\n  }\n  throw e;\n}","preventionTips":["Hit the Google token endpoint directly over HTTPS without proxy interference.","Log unexpected response bodies in development to detect MITM/captive portals.","Retry idempotent token refreshes with small backoff.","Pin the exact OAuth endpoint URL and verify TLS."],"tags":["gmail","oauth","unexpected-response-shape","google"],"backgroundTag":"unexpected-response-shape","analyzedSha":"aac998c7ed7248ea853b720ceeb1fdbeb322fc5d","analyzedAt":"2026-09-16T06:35:29.214Z","contentChangedAt":"2026-09-16T06:35:29.214Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}