Skip to content

fix: retry transient Gmail 500s when creating block-sender filters#172

Open
ankitvgupta wants to merge 3 commits into
mainfrom
ankitvgupta/fix-block-sender-error
Open

fix: retry transient Gmail 500s when creating block-sender filters#172
ankitvgupta wants to merge 3 commits into
mainfrom
ankitvgupta/fix-block-sender-error

Conversation

@ankitvgupta

@ankitvgupta ankitvgupta commented Jun 12, 2026

Copy link
Copy Markdown
Owner

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.create POST, and Gmail's settings backend intermittently returns 500 backendError when those creates land within ~1.5s of each other. gaxios (the googleapis HTTP layer) never retries POSTs by default — its httpMethodsToRetry is GET, 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):

Time Sender Result
10:02:49.1 sender A ✅ blocked
+0.5s sender B ❌ Gmail 500 backendError
+1.4s sender C ❌ Gmail 500 backendError
+13s sender D ✅ blocked

Changes

  • Extract createBlockFilter into src/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.createBlockFilter now delegates to it.
  • Opt the create POST into gaxios retry via per-request 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 retry backendError with backoff.
  • Resolve duplicate-filter rejections after ambiguous 500s: a Gmail 500 doesn't guarantee the write failed, so a retried create can be rejected with 400 "Filter already exists". We now look up the existing filter via filters.list (matching criteria.from + TRASH action) and return its ID instead of failing the block.
  • Friendlier toast for persistent 5xx in sync.ipc.ts: "Gmail temporary server error — please try again" instead of Gmail's raw "Internal error encountered."
  • New unit tests (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 — clean
  • npm run test:unit — 1431 passed (6 new)
  • npm run test:integration — 18 passed
  • npm run test:e2e — 343 passed, 0 failed
  • npm run pre-pr (full) — report injected below

Note: the dedicated test account's refresh token predates the gmail.settings.basic scope, 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


Open in Devin Review

Pre-PR verdict: FAIL

  • mode: full
  • sha: 7fa9397
  • generated: 2026-06-12T18:19:36.723Z
Phase Status Duration
eval:analyzer ✅ exit 0 27.4s
eval:features ✅ exit 0 51.5s
agentic-verify ❌ exit 124 599.9s
real-gmail:cached ✅ exit 0 24.8s

Failures

agentic-verify — exit 124
[electron] [14:12:06.764] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14232�[39m
[electron] [14:12:06.767] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14232�[39m
[electron] [14:12:30.302] �[32mINFO�[39m (16227): �[36m[Block] Blocked promo@shopco.test (filterId=ANe1Bmjn81ZE7R5VB386C-qjU-NTxiYTd9yAng, moved=1)�[39m
[electron] [14:12:35.567] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:12:36.765] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14232�[39m
[electron] [14:12:36.765] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14232�[39m
[electron] [14:12:38.520] �[32mINFO�[39m (16227): �[36m[Sync] 1 emails removed for exoemailtest@gmail.com�[39m
[electron] [14:13:06.769] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:13:06.782] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:13:36.772] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:13:36.773] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:13:54.034] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:14:06.774] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:14:06.775] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:14:36.191] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:14:36.774] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:14:36.775] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:15:06.776] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:15:06.778] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:15:35.487] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:15:36.780] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:15:36.782] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:16:06.781] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:16:06.790] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:16:36.171] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:16:36.786] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:16:36.786] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:17:06.788] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:17:06.792] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:17:35.758] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:17:36.790] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:17:36.791] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:18:06.795] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:18:06.796] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:18:35.676] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:18:36.799] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:18:36.799] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:19:06.802] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:19:06.805] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m

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-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes intermittent "Block failed" toasts caused by Gmail's settings backend returning transient 500 backendError responses when multiple filters.create POSTs land in quick succession. The core logic is extracted into a testable, Electron-free module (gmail-block-filter.ts) and a gaxios per-request retryConfig opts POST into exponential-backoff retry.

  • gmail-block-filter.ts — new module with retry config (4 retries, 429/5xx), duplicate-filter recovery (if a retried create lands after a server-side success, filters.list resolves the existing ID), and httpErrorStatus utility; GmailClient.createBlockFilter now delegates here.
  • sync.ipc.ts — friendlier toast for persistent 5xx ("Gmail temporary server error — please try again") instead of Gmail's raw "Internal error encountered."
  • tests/unit/gmail-block-filter.spec.ts — 6 MSW-backed tests that exercise the real googleapis/gaxios retry stack: clean create, retry-then-succeed, exhausted retries, duplicate-then-resolve, duplicate-then-rethrow, and list-fallback failure.

Confidence Score: 5/5

Safe 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.

Important Files Changed

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"
Loading

Reviews (3): Last reviewed commit: "pre-pr: give agentic-verify 20 min (bloc..." | Re-trigger Greptile

greptile-apps[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@ankitvgupta

ankitvgupta commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

❌ Pre-PR verification — FAIL

  • mode: full
  • sha: 7fa9397
  • generated: 2026-06-12T18:19:47.753Z
Phase Status Duration
eval:analyzer ✅ exit 0 27.4s
eval:features ✅ exit 0 51.5s
agentic-verify ❌ exit 124 599.9s
real-gmail:cached ✅ exit 0 24.8s

Agentic verification report not found — likely the phase failed before writing its report. See logs below.

Failures

agentic-verify — exit 124
[electron] [14:12:06.764] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14232�[39m
[electron] [14:12:06.767] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14232�[39m
[electron] [14:12:30.302] �[32mINFO�[39m (16227): �[36m[Block] Blocked promo@shopco.test (filterId=ANe1Bmjn81ZE7R5VB386C-qjU-NTxiYTd9yAng, moved=1)�[39m
[electron] [14:12:35.567] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:12:36.765] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14232�[39m
[electron] [14:12:36.765] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14232�[39m
[electron] [14:12:38.520] �[32mINFO�[39m (16227): �[36m[Sync] 1 emails removed for exoemailtest@gmail.com�[39m
[electron] [14:13:06.769] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:13:06.782] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:13:36.772] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:13:36.773] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:13:54.034] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:14:06.774] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:14:06.775] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:14:36.191] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:14:36.774] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:14:36.775] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:15:06.776] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:15:06.778] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:15:35.487] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:15:36.780] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:15:36.782] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:16:06.781] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:16:06.790] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:16:36.171] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:16:36.786] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:16:36.786] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:17:06.788] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:17:06.792] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:17:35.758] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:17:36.790] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:17:36.791] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:18:06.795] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:18:06.796] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:18:35.676] �[31mERROR�[39m (16227): �[36m[CalendarSync] Failed to sync account default�[39m
[electron] [14:18:36.799] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:18:36.799] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m
[electron] [14:19:06.802] �[32mINFO�[39m (16227): �[36m[Sync] syncAccount called for exoemailtest@gmail.com, historyId=14271�[39m
[electron] [14:19:06.805] �[32mINFO�[39m (16227): �[36m[Sync] INCREMENTAL sync for exoemailtest@gmail.com from history 14271�[39m

This comment is upserted by npm run pre-pr. The CI gate reads the marker block in the PR description, not this comment.

Ankit Gupta and others added 2 commits June 12, 2026 13:19
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant