{"record":{"id":"9c2cea83b3d20932","repo":"santifer/career-ops","slug":"gmail-token-refresh-failed-res-status-await","errorCode":null,"errorMessage":"Gmail token refresh failed: ${res.status} ${(await res.text()).slice(0, 200)}","messagePattern":"Gmail token refresh failed: (.+?) (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/gmail/index.mjs","lineNumber":44,"sourceCode":"\nconst TOKEN_URL = 'https://oauth2.googleapis.com/token';\nconst GMAIL_API = 'https://gmail.googleapis.com/gmail/v1/users/me';\nconst 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 {","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/plugins/gmail/index.mjs#L26-L62","documentation":"Thrown by `getAccessToken` in the gmail plugin (plugins/gmail/index.mjs:44) when the OAuth2 token-refresh POST to Google's token endpoint returns a non-OK HTTP status. The message includes the status code and the first 200 chars of the response body for diagnosis. Common underlying causes: an expired/revoked refresh token, wrong client_id/client_secret, or Google rate-limiting. This is the refresh step that exchanges a refresh_token for a short-lived access_token used to call the Gmail API.","triggerScenarios":"The gmail plugin calls `getAccessToken({ clientId, clientSecret, refreshToken })`; the POST to TOKEN_URL returns 400 (invalid_grant — refresh token expired/revoked), 401 (bad client credentials), or 429/5xx. `if (!res.ok)` fires and the error is thrown with the body snippet.","commonSituations":"Refresh token revoked (user revoked access, or it expired after 6 months of inactivity); client_id/client_secret mismatch after a Google Cloud project change; clock skew causing invalid_grant; Google rate-limiting token refreshes; the OAuth app in testing mode with expired consent.","solutions":["Read the body snippet: `invalid_grant` → re-run the OAuth flow to get a fresh refresh token; `invalid_client` → fix client_id/client_secret.","Re-authorize: redo the gmail plugin's OAuth setup to obtain a new refresh token.","Verify client_id/client_secret in .env match the Google Cloud OAuth client.","If 429/5xx, wait and retry with backoff.","Check system clock sync (`invalid_grant` can be caused by clock skew)."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Validate refresh-token config shape before calling getAccessToken.\nfunction assertGmailCreds(c) {\n  for (const k of ['clientId', 'clientSecret', 'refreshToken']) {\n    if (typeof c[k] !== 'string' || !c[k]) {\n      throw new Error(`Gmail OAuth config missing ${k}.`);\n    }\n  }\n}\nassertGmailCreds(gmailConfig);","typeGuard":"/** @param {unknown} c */\nfunction isValidGmailCreds(c) {\n  return c != null &&\n    typeof c.clientId === 'string' && typeof c.clientSecret === 'string' &&\n    typeof c.refreshToken === 'string' &&\n    c.clientId && c.clientSecret && c.refreshToken;\n}","tryCatchPattern":"async function refreshWithRetry(creds, retries = 2) {\n  for (let i = 0; i <= retries; i++) {\n    try { return await getAccessToken(creds); }\n    catch (err) {\n      const transient = /token refresh failed: (429|5\\d\\d)/.test(err.message);\n      if (transient && i < retries) { await new Promise(r => setTimeout(r, 1000 * (i + 1))); continue; }\n      if (/invalid_grant|invalid_client/.test(err.message)) throw err; // needs re-auth, not retry\n      throw err;\n    }\n  }\n}","preventionTips":["Re-authorize proactively before refresh tokens expire (~6 months idle).","Keep client_id/client_secret in sync with the Google Cloud OAuth client.","Sync the system clock — skew causes invalid_grant."],"tags":["gmail","oauth","auth","network","secrets"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}