{"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":"http","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/aac998c7ed7248ea853b720ceeb1fdbeb322fc5d/plugins/gmail/index.mjs#L26-L62","documentation":"getAccessToken exchanges a Gmail OAuth refresh token for an access token via Google's token endpoint. If the HTTP response is not ok, it throws with the status code and the first 200 chars of the error body.","triggerScenarios":"POST to https://oauth2.googleapis.com/token returns 400/401/403 — invalid or revoked refresh token, wrong client_id/client_secret, malformed grant request, or account access revoked.","commonSituations":"Refresh token revoked because the app is in 'Testing' mode and the token expired after 7 days; client secret rotated or mismatched with the one used to issue the refresh token; typo'd credentials in .env; Google returning invalid_grant.","solutions":["Read the error body in the message — invalid_grant usually means the refresh token is expired/revoked: re-run the OAuth consent flow to get a new refresh token.","Verify GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN in .env match the Google Cloud OAuth client.","If the OAuth app is in Testing mode, either publish it or regenerate the refresh token weekly (testing tokens expire after 7 days).","Confirm no whitespace/quotes are corrupting the .env values.","Check clock skew on the machine — invalid_grant can be caused by large time drift."],"exampleFix":"// before\nrefresh_token: refreshToken, // token revoked (app in Testing mode)\n// after\n// regenerate via OAuth playground / consent flow with your published client:\nrefresh_token: process.env.GMAIL_REFRESH_TOKEN, // freshly issued token\n// and move the OAuth app out of Testing mode or re-consent every 7 days","handlingStrategy":"retry","validationCode":"const creds = ['GMAIL_CLIENT_ID','GMAIL_CLIENT_SECRET','GMAIL_REFRESH_TOKEN'];\nif (creds.some(k => !process.env[k])) throw new Error('missing Gmail OAuth credentials');","typeGuard":null,"tryCatchPattern":"try {\n  const token = await getAccessToken();\n} catch (e) {\n  if (String(e.message).includes('token refresh failed')) {\n    if (e.message.includes('invalid_grant')) {\n      console.error('Refresh token expired/revoked — re-run OAuth consent flow');\n    }\n    // transient 5xx: retry with backoff\n    return retryWithBackoff(getAccessToken, 3);\n  }\n  throw e;\n}","preventionTips":["Publish the OAuth app (or re-consent weekly while in Testing mode).","Store the refresh token securely and regenerate after rotating client secrets.","Log the first 200 chars of error bodies to distinguish invalid_grant from transient errors.","Keep .env values free of stray quotes/whitespace."],"tags":["gmail","oauth","token-refresh","google"],"backgroundTag":"oauth-token-exchange-failed","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"}