fix: retry transient Gmail 500s when creating block-sender filters - #172
Conversation
Blocking several senders back-to-back fails with "Failed to create Gmail filter: Internal error encountered." — production logs show Gmail's settings backend returns 500 backendError when filters.create POSTs land within ~1.5s of each other (each undo-toast commit fires its own create), and gaxios never retries POSTs by default. - Extract createBlockFilter into gmail-block-filter.ts (Electron-free so tests can drive the real googleapis/gaxios stack via MSW) and opt the POST into gaxios retry on 429/5xx with exponential backoff. - A Gmail 500 doesn't guarantee the write failed, so a retried create can be rejected with 400 "Filter already exists" — resolve to the existing filter's ID via filters.list instead of failing the block. - Map remaining 5xx errors to a "temporary server error — please try again" toast instead of Gmail's raw "Internal error encountered." Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile SummaryThis PR fixes intermittent "Block failed" toasts caused by Gmail's settings backend returning transient 500
Confidence Score: 5/5Safe to merge — the change is well-scoped to the filter-creation path, all recovery branches are covered by MSW-backed tests that exercise the real gaxios retry stack, and the fallback behavior (re-throwing the original error) is preserved if the list lookup itself fails. The retry logic, duplicate-filter recovery, and friendlier error message are each handled by dedicated, passing unit tests. The findBlockFilterId fallback correctly guards its own errors with a try/catch so the original duplicate error surfaces rather than a secondary lookup failure. No pre-existing behavior changes outside the filter-creation code path. No files require special attention.
|
| Filename | Overview |
|---|---|
| src/main/services/gmail-block-filter.ts | New module implementing retry logic, duplicate-filter recovery (try/catch guards the list fallback to preserve original error), and the httpErrorStatus utility — logic is clean and well-tested. |
| src/main/ipc/sync.ipc.ts | Adds friendlier 5xx toast using httpErrorStatus; non-5xx errors still surface the raw message, matching prior behavior — safe change. |
| src/main/services/gmail-client.ts | createBlockFilter now delegates to gmail-block-filter.ts; no behavioral change to GmailClient's interface. |
| tests/unit/gmail-block-filter.spec.ts | 6 MSW-backed tests covering all major paths: clean create, retry-then-succeed, exhausted-retry, duplicate recovery, missing-match rethrow, and list-fallback failure — comprehensive coverage of the new retry and recovery logic. |
| scripts/pre-pr.mjs | Timeout bump for agentic-verify from 10m to 20m to accommodate the block-sender undo-toast flow; well-justified by the PR #172 incident comment. |
Sequence Diagram
sequenceDiagram
participant IPC as sync.ipc.ts
participant BF as gmail-block-filter.ts
participant GA as gaxios (retry)
participant GM as Gmail API
IPC->>BF: createBlockFilter(email)
BF->>GA: "filters.create POST (retryConfig: retry=4, POST, 429/5xx)"
GA->>GM: POST /filters
GM-->>GA: 500 backendError
GA->>GM: POST /filters (retry 1, backoff)
GM-->>GA: 500 backendError (transient)
GA->>GM: POST /filters (retry 2, backoff)
GM-->>GA: "200 {id: filter-xyz}"
GA-->>BF: response.data.id
BF-->>IPC: filterId ✅
note over GA,GM: Ambiguous-500 path
GA->>GM: POST /filters (retry after 500)
GM-->>GA: 400 Filter already exists
GA-->>BF: GaxiosError 400
BF->>GM: GET /filters (findBlockFilterId)
GM-->>BF: filter list
BF-->>IPC: existingId ✅
note over IPC,BF: All retries exhausted
BF-->>IPC: GaxiosError 500
IPC-->>IPC: "httpErrorStatus >= 500 → friendly toast"
Reviews (3): Last reviewed commit: "pre-pr: give agentic-verify 20 min (bloc..." | Re-trigger Greptile
✅ Pre-PR verification — PASS
Agentic verification — summaryAgentic verification — verify-diff
Summarycategory=C. The PR refactors createBlockFilter out of gmail-client.ts into a dedicated gmail-block-filter.ts module that adds POST retry config (4 retries on 5xx/429) and duplicate-filter fallback. Also improves the 5xx error message in sync.ipc.ts. Verified by opening a ConfBoston email (cfp@confboston.test) and clicking 'Block sender' in the sidebar panel. The flow executed end-to-end in real mode: the toast 'Blocked cfp@confboston.test. Undo' appeared immediately, both ConfBoston emails were removed from the inbox (count 28→26 confirming batchMoveToTrash ran), and a direct call to window.api.emails.listBlockedSenders confirmed a real Gmail filter ID (ANe1BmiVdGBeVBwtRXYguVAPyNCM_ZzY7eyHJA) was created and persisted in the local DB. Zero JS errors throughout. The pre-pr timeout bump (10→20 min) in scripts/pre-pr.mjs is infra-only with no exercisable UI surface. Agentic verification — literal trace[2026-08-06T19:02:10.720Z] tool#13: mcp__chrome-devtools__click {"errorCount":0,"errors":[],"allTabText":"not found"}[2026-08-06T19:02:38.288Z] tool#16: mcp__chrome-devtools__evaluate_script {"error":"TypeError: window.api.sync.getBlockedSenders is not a function"}[2026-08-06T19:02:45.636Z] tool#17: mcp__chrome-devtools__evaluate_script {"syncMethods":["init","start","stop","now","status","setInterval","getEmails","getSentEmails","prefetchBodies","onNewEmails","onNewSentEmails","onStatusChange","onEmailsRemoved","onEmailsUpdated","onDraftsRemoved","onActionFailed","onActionSucceeded","onSyncProgress","removeAllListeners"]}[2026-08-06T19:02:50.795Z] tool#18: mcp__chrome-devtools__evaluate_script {"apiKeys":["_debugLog","gmail","analysis","drafts","compose","emails","style","contacts","search","settings","memory","sender","onboarding","accounts","sync","prefetch","backgroundSync","splits","snippets","theme","auth","extensions","archiveReady","snooze","network","scheduledSend","calendar","attachments","agent","defaultMailApp","updates","outbox","find","usage"],"blockRelated":{"emails":["archive","batchArchive","archiveThread","trash","batchTrash","setStarred","setRead","getThread","search","searchRemote","blockSender","unblockSender","listBlockedSenders"]}}[2026-08-06T19:02:55.510Z] text: Found it — {"accountId":"default","accounts":["default"],"blockedResult":{"success":true,"data":[{"senderEmail":"cfp@confboston.test","accountId":"default","gmailFilterId":"ANe1BmiVdGBeVBwtRXYguVAPyNCM_ZzY7eyHJA","blockedAt":1786042937059},{"senderEmail":"updates@tools-inc.test","accountId":"default","gmailFilterId":"ANe1BmgAB-JCaBqNfhAy5jWfRoEO01vkdm0Q7Q","blockedAt":1781291045504}]}}[2026-08-06T19:03:32.993Z] text: The block sender flow worked end-to-end with concrete evidence:
Let me do a final check for console errors and take a screenshot to confirm the email count change. {"exoErrors":0,"currentURL":"http://localhost:5173/"}[2026-08-06T19:03:45.293Z] text: All checks pass. Let me take a final screenshot to document the state. Summary of evidence collected:
{"verdict":"pass","summary":"category=C. The PR refactors createBlockFilter out of gmail-client.ts into a dedicated gmail-block-filter.ts module that adds POST retry config (4 retries on 5xx/429) and duplicate-filter fallback. Also improves the 5xx error message in sync.ipc.ts. Verified by opening a ConfBoston email (cfp@confboston.test) and clicking 'Block sender' in the sidebar panel. The flow executed end-to-end in real mode: the toast 'Blocked cfp@confboston.test. Undo' appeared immediately, both ConfBoston emails were removed from the inbox (count 28→26 confirming batchMoveToTrash ran), and a direct call to window.api.emails.listBlockedSenders confirmed a real Gmail filter ID (ANe1BmiVdGBeVBwtRXYguVAPyNCM_ZzY7eyHJA) was created and persisted in the local DB. Zero JS errors throughout. The pre-pr timeout bump (10→20 min) in scripts/pre-pr.mjs is infra-only with no exercisable UI surface.","anomalies":[],"actions_taken":15} |
- Preserve the original duplicate error when the filters.list fallback lookup itself fails (Greptile) - Accept numeric .code as HTTP status fallback in httpErrorStatus, matching the existing gmail-client.ts defensive pattern (Devin) - Cite gaxios 7's actual backoff formula in the retry comment (Greptile questioned the ~5.6s figure; the formula and the new exhausted-retry test runtime both confirm it) - Add tests: exhausted-retry path rejects with 5xx after 5 POSTs; lookup-failure path rethrows the original duplicate error (Greptile) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The verify-diff agent successfully exercised block-sender end-to-end (filter created on real Gmail, email removed) but hit the 10-min hard timeout before emitting its verdict. The flow's undo-toast commit delay plus real-Gmail round-trips put honest runs past 10 min of slow model turns. Mirrors the existing --budget-usd bump just above. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-sender-error # Conflicts: # scripts/pre-pr.mjs
Summary
Blocking a sender intermittently fails with the toast "Block failed for …: Failed to create Gmail filter: Internal error encountered." Production logs pinpointed the cause: when several senders are blocked back-to-back, each undo-toast commit fires its own
users.settings.filters.createPOST, and Gmail's settings backend intermittently returns 500backendErrorwhen those creates land within ~1.5s of each other. gaxios (the googleapis HTTP layer) never retries POSTs by default — itshttpMethodsToRetryisGET, HEAD, PUT, OPTIONS, DELETE— so a single transient 500 surfaced directly to the user as a failed block.Observed timeline from the incident (same account, same minute):
backendErrorbackendErrorChanges
createBlockFilterintosrc/main/services/gmail-block-filter.ts— a pure, Electron-free module so unit tests can drive the real googleapis/gaxios stack (gmail-client.ts transitively imports Electron and can't be loaded in tests).GmailClient.createBlockFilternow delegates to it.retryConfig: retry on 429/5xx, 4 attempts with exponential backoff (last retry lands ~5.6s after the first attempt, past the observed collision window). This follows Google's documented guidance to retrybackendErrorwith backoff.filters.list(matchingcriteria.from+ TRASH action) and return its ID instead of failing the block.sync.ipc.ts: "Gmail temporary server error — please try again" instead of Gmail's raw "Internal error encountered."tests/unit/gmail-block-filter.spec.ts) using MSW against the real googleapis client: clean create (no retry), retry-then-succeed on 500s, no retry on non-retryable 400, duplicate→existing-ID resolution, duplicate rethrow when no match, and HTTP status extraction.Test plan
npm run typecheck,npm run lint,npm run format:check— cleannpm run test:unit— 1431 passed (6 new)npm run test:integration— 18 passednpm run test:e2e— 343 passed, 0 failednpm run pre-pr(full) — report injected belowNote: the dedicated test account's refresh token predates the
gmail.settings.basicscope, so dev-mode agentic verification cannot exercise filter creation end-to-end (it 403s at Gmail regardless of this change). The MSW tests exercise the production code path against the real HTTP/retry stack instead.🤖 Generated with Claude Code
Pre-PR verdict: PASS
full4cfb01a