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

  1. Read the body snippet: `invalid_grant` → re-run the OAuth flow to get a fresh refresh token; `invalid_client` → fix client_id/client_secret.
  2. Re-authorize: redo the gmail plugin's OAuth setup to obtain a new refresh token.
  3. Verify client_id/client_secret in .env match the Google Cloud OAuth client.
  4. If 429/5xx, wait and retry with backoff.
  5. 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

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


AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13). Data as JSON: /api/errors/9c2cea83b3d20932. Report an issue: GitHub.