fix: retry transient Gmail 500s when creating block-sender filters#172
fix: retry transient Gmail 500s when creating block-sender filters#172ankitvgupta wants to merge 3 commits into
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 — FAIL
Agentic verification report not found — likely the phase failed before writing its report. See logs below. Failuresagentic-verify — exit 124This comment is upserted by |
- 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>
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: FAIL
full7fa9397Failures
agentic-verify — exit 124