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
- 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
- Re-run ingest after fixing auth/rate limits — the failed message will be retried since it was never added to processed IDs
- Reduce batch size or add a small delay per message to stay under Gmail API rate limits
- 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
- Refresh the OAuth token before long batches; ensure gmail.readonly scope
- Add small delays/backoff between detail fetches to respect Gmail rate limits
- Re-run ingest after transient failures — unprocessed ids retry automatically
- Treat the warning stream as the record of skipped messages and reconcile counts
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
- Notion ${method} ${path} -> ${j.code}: ${j.message}
- themuse: unexpected API response on page ${page} — expected
- vc-portfolios: YC API fetch failed — ${err.message}
- [models] Failed to fetch free model list: ${reason}. Check t
- HTTP ${resp.status}: ${t.slice(0, 120)}
AI-assisted analysis of santifer/career-ops@1696bec4d0 (2026-09-01).
Data as JSON: /api/errors/7be53d4b0064c246.
Report an issue: GitHub.