{"record":{"id":"7be53d4b0064c246","repo":"santifer/career-ops","slug":"gmail-failed-to-fetch-message-m-id-err-mes","errorCode":null,"errorMessage":"gmail: failed to fetch message ${m.id} — ${err.message}","messagePattern":"gmail: failed to fetch message (.+?) — (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"plugins/gmail/index.mjs","lineNumber":114,"sourceCode":"      let url = `${GMAIL_API}/messages?q=${encodeURIComponent(query)}`;\n      if (pageToken) url += `&pageToken=${pageToken}`;\n      const data = await (await ctx.fetch(url, { headers: auth })).json();\n      if (data.messages) messages.push(...data.messages);\n      pageToken = data.nextPageToken;\n    } while (pageToken);\n\n    const processedIds = loadProcessedIds();\n    const seenUrls = new Set();\n    const jobs = [];\n\n    for (const m of messages) {\n      if (processedIds.has(m.id)) continue;\n      // Per-message resilience: a single bad detail fetch is skipped, not fatal.\n      let msg;\n      try {\n        msg = await (await ctx.fetch(`${GMAIL_API}/messages/${m.id}?format=full`, { headers: auth })).json();\n      } catch (err) {\n        console.warn(`gmail: failed to fetch message ${m.id} — ${err.message}`);\n        continue;\n      }\n      const headers = msg.payload?.headers || [];\n      const subject = headers.find(h => h.name?.toLowerCase() === 'subject')?.value || '';\n\n      // Fail-closed on spoofed mail (DMARC).\n      if (!isAuthenticEmail(headers)) {\n        console.warn(`gmail: skipping spoofed/unauthenticated email \"${subject}\"`);\n        processedIds.add(m.id);\n        continue;\n      }\n\n      const seed = parseRoleAtCompany(subject);\n      const cleanUrls = extractUrls(getMessageBody(msg.payload)).filter(isCleanUrl);\n      for (const url of cleanUrls) {\n        if (seenUrls.has(url)) continue;\n        seenUrls.add(url);\n        jobs.push({","sourceCodeStart":96,"sourceCodeEnd":132,"githubUrl":"https://github.com/santifer/career-ops/blob/1696bec4d021768e7359f9aad6b329cba883da20/plugins/gmail/index.mjs#L96-L132","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nmsg = await (await ctx.fetch(`${GMAIL_API}/messages/${m.id}?format=full`, { headers: auth })).json();\n// 429 rate limit skips the message\n// after: retry transient failures before giving up\nlet msg;\nfor (let attempt = 0; attempt < 3; attempt++) {\n  try {\n    msg = await (await ctx.fetch(`${GMAIL_API}/messages/${m.id}?format=full`, { headers: auth })).json();\n    break;\n  } catch (err) {\n    if (attempt === 2) { console.warn(`gmail: failed to fetch message ${m.id} — ${err.message}`); }\n    else await new Promise(r => setTimeout(r, 1000 * 2 ** attempt));\n  }\n}","handlingStrategy":"retry","validationCode":"// Pre-flight: verify auth before batching detail fetches\nconst probe = await ctx.fetch(`${GMAIL_API}/messages/${list[0].id}?format=full`, { headers: auth });\nif (!probe.ok) throw new Error(`auth/rate problem: HTTP ${probe.status} — fix before ingest`);","typeGuard":null,"tryCatchPattern":"try {\n  const items = await gmailPlugin.ingest(ctx);\n} catch (err) {\n  // ingest itself is resilient; this only fires on list-level failure\n  console.error(`gmail ingest aborted: ${err.message}`);\n}\n// per-message skips are warnings — re-run ingest to pick up skipped ids (they were never marked processed)","preventionTips":["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"],"tags":["gmail","api","resilience","rate-limit"],"backgroundTag":"http-non-2xx-response","analyzedSha":"1696bec4d021768e7359f9aad6b329cba883da20","analyzedAt":"2026-09-01T19:19:23.111Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}