santifer/career-ops · error · Error
Gmail token refresh failed: ${res.status} ${(await res.text(
Error message
Gmail token refresh failed: ${res.status} ${(await res.text()).slice(0, 200)} What it means
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.
Source
Thrown at plugins/gmail/index.mjs:44
const TOKEN_URL = 'https://oauth2.googleapis.com/token';
const GMAIL_API = 'https://gmail.googleapis.com/gmail/v1/users/me';
const STATE_PATH = 'data/gmail-state.json'; // the plugin's own processed-id cursor
/** Exchange the long-lived refresh token for a short-lived access token. */
async function getAccessToken({ clientId, clientSecret, refreshToken }, fetchFn = globalThis.fetch) {
const res = await fetchFn(TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
refresh_token: refreshToken,
grant_type: 'refresh_token',
}),
});
if (!res.ok) {
throw new Error(`Gmail token refresh failed: ${res.status} ${(await res.text()).slice(0, 200)}`);
}
const data = await res.json();
if (!data.access_token) throw new Error('Gmail token refresh returned no access_token');
return data.access_token;
}
function loadProcessedIds() {
if (!existsSync(STATE_PATH)) return new Set();
try {
const state = JSON.parse(readFileSync(STATE_PATH, 'utf-8'));
return new Set(state.processed_message_ids || []);
} catch {
return new Set();
}
}
function saveProcessedIds(ids) {
try {View on GitHub (pinned to 9b17a8ac97)
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).
Defensive patterns
Strategy: retry
Validate before calling
// Validate refresh-token config shape before calling getAccessToken.
function assertGmailCreds(c) {
for (const k of ['clientId', 'clientSecret', 'refreshToken']) {
if (typeof c[k] !== 'string' || !c[k]) {
throw new Error(`Gmail OAuth config missing ${k}.`);
}
}
}
assertGmailCreds(gmailConfig); Type guard
/** @param {unknown} c */
function isValidGmailCreds(c) {
return c != null &&
typeof c.clientId === 'string' && typeof c.clientSecret === 'string' &&
typeof c.refreshToken === 'string' &&
c.clientId && c.clientSecret && c.refreshToken;
} Try / catch
async function refreshWithRetry(creds, retries = 2) {
for (let i = 0; i <= retries; i++) {
try { return await getAccessToken(creds); }
catch (err) {
const transient = /token refresh failed: (429|5\d\d)/.test(err.message);
if (transient && i < retries) { await new Promise(r => setTimeout(r, 1000 * (i + 1))); continue; }
if (/invalid_grant|invalid_client/.test(err.message)) throw err; // needs re-auth, not retry
throw err;
}
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Gmail token refresh returned no access_token
- APIFY_TOKEN not set
- APIFY_TOKEN not set — enable apify in config/plugins.yml and
- gmail: missing GMAIL_CLIENT_ID / GMAIL_CLIENT_SECRET / GMAIL
- Notion ${method} ${path} -> ${j.code}: ${j.message}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/9c2cea83b3d20932.
Report an issue: GitHub.