santifer/career-ops · warning

gmail: failed to fetch message ${m.id} — ${err.message}

Error message

gmail: failed to fetch message ${m.id} — ${err.message}

What it means

The gmail plugin's ingest fetches each message's full detail (format=full) individually. A single failed detail fetch is deliberately non-fatal: the plugin logs 'gmail: failed to fetch message <id> — <cause>' and continues with the next message, so one bad message never aborts the whole batch. This means items can be silently skipped from the ingest results.

Source

Thrown at plugins/gmail/index.mjs:114

      let url = `${GMAIL_API}/messages?q=${encodeURIComponent(query)}`;
      if (pageToken) url += `&pageToken=${pageToken}`;
      const data = await (await ctx.fetch(url, { headers: auth })).json();
      if (data.messages) messages.push(...data.messages);
      pageToken = data.nextPageToken;
    } while (pageToken);

    const processedIds = loadProcessedIds();
    const seenUrls = new Set();
    const jobs = [];

    for (const m of messages) {
      if (processedIds.has(m.id)) continue;
      // Per-message resilience: a single bad detail fetch is skipped, not fatal.
      let msg;
      try {
        msg = await (await ctx.fetch(`${GMAIL_API}/messages/${m.id}?format=full`, { headers: auth })).json();
      } catch (err) {
        console.warn(`gmail: failed to fetch message ${m.id} — ${err.message}`);
        continue;
      }
      const headers = msg.payload?.headers || [];
      const subject = headers.find(h => h.name?.toLowerCase() === 'subject')?.value || '';

      // Fail-closed on spoofed mail (DMARC).
      if (!isAuthenticEmail(headers)) {
        console.warn(`gmail: skipping spoofed/unauthenticated email "${subject}"`);
        processedIds.add(m.id);
        continue;
      }

      const seed = parseRoleAtCompany(subject);
      const cleanUrls = extractUrls(getMessageBody(msg.payload)).filter(isCleanUrl);
      for (const url of cleanUrls) {
        if (seenUrls.has(url)) continue;
        seenUrls.add(url);
        jobs.push({

View on GitHub (pinned to 1696bec4d0)

Solutions

  1. Read err.message for the cause: 404 → message is gone, safe to ignore (or mark the id processed to avoid retrying); 401/403 → refresh the OAuth token or add the gmail.readonly scope; 429 → add backoff between detail fetches
  2. Re-run ingest after fixing auth/rate limits — the failed message will be retried since it was never added to processed IDs
  3. Reduce batch size or add a small delay per message to stay under Gmail API rate limits
  4. If many messages fail, check Gmail API status and network egress from the environment before assuming per-message issues

Example fix

// before
msg = await (await ctx.fetch(`${GMAIL_API}/messages/${m.id}?format=full`, { headers: auth })).json();
// 429 rate limit skips the message
// after: retry transient failures before giving up
let msg;
for (let attempt = 0; attempt < 3; attempt++) {
  try {
    msg = await (await ctx.fetch(`${GMAIL_API}/messages/${m.id}?format=full`, { headers: auth })).json();
    break;
  } catch (err) {
    if (attempt === 2) { console.warn(`gmail: failed to fetch message ${m.id} — ${err.message}`); }
    else await new Promise(r => setTimeout(r, 1000 * 2 ** attempt));
  }
}
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight: verify auth before batching detail fetches
const probe = await ctx.fetch(`${GMAIL_API}/messages/${list[0].id}?format=full`, { headers: auth });
if (!probe.ok) throw new Error(`auth/rate problem: HTTP ${probe.status} — fix before ingest`);

Try / catch

try {
  const items = await gmailPlugin.ingest(ctx);
} catch (err) {
  // ingest itself is resilient; this only fires on list-level failure
  console.error(`gmail ingest aborted: ${err.message}`);
}
// per-message skips are warnings — re-run ingest to pick up skipped ids (they were never marked processed)

Prevention

When it happens

Trigger: Calling ingest when GET {GMAIL_API}/messages/{id}?format=full fails for a specific message: the message was deleted/trashed between the list and detail calls (404), the auth token lacks the gmail.readonly scope or expired mid-run (401/403), rate limiting (429), or a transient network error via ctx.fetch.

Common situations: Messages deleted by another client after listing; OAuth token expiry during a long batch; Gmail API per-user rate limits tripped by large mailboxes; sandboxed environments where ctx.fetch can't reach gmail.googleapis.com.

Related errors


AI-assisted analysis of santifer/career-ops@1696bec4d0 (2026-09-01). Data as JSON: /api/errors/7be53d4b0064c246. Report an issue: GitHub.