Skip to content

Fix unhelpful mailsync crash message and Sentry noise from OAuth SSL failures - #2771

Merged
bengotow merged 4 commits into
masterfrom
claude/awesome-ritchie-i9utje
Jul 16, 2026
Merged

Fix unhelpful mailsync crash message and Sentry noise from OAuth SSL failures#2771
bengotow merged 4 commits into
masterfrom
claude/awesome-ritchie-i9utje

Conversation

@bengotow

Copy link
Copy Markdown
Collaborator

Fixes MAILSPRING-CLIENT-Q

What I observed

Sentry issue MAILSPRING-CLIENT-Q ("An unknown error has occurred mailsync: null.") had 121 occurrences across 35 users over 30 days. Sampling ~15 individual events showed:

  • Every single one has the identical culprit/stack: MailsyncProcess._spawnAndWait's close handler in app/src/mailsync-process.ts, triggered from the OAuth onboarding flow (onboarding-helpers.tsfinalizeAndValidateAccountproc.test()).
  • The embedded mailsync log always shows the same failure signature:
    info: Fetching XOAuth2 access token (outlook) for ...
    critical: *** An exception occurred during program execution:
    *** {"debuginfo":"https://login.microsoftonline.com/common/oauth2/v2.0/token","key":"SSL connect error","offline":true,"retryable":true,"what":"std::exception"}
    
    with a C++ stack trace through MakeOAuthRefreshRequestPerformRequestValidateRequestResp(CURLcode, ...).
  • ~13 of 15 sampled events came from users in mainland China (remaining from Russia / DR Congo) — consistent with local network interference/TLS interception blocking login.microsoftonline.com, not an app bug.
  • Because mailsync crashes on this uncaught exception instead of exiting cleanly, the Node close event fires with code === null and no JSON response. _spawnAndWait's handler only read the code param (ignoring signal), so the resulting error was a useless "An unknown error has occurred mailsync: null. <raw crash log>" — no signal info, and a wall of C++ stack trace text as the "message".
  • That raw error then reached Sentry because oauth-signin-page.tsx's _onError already has a precedent for skipping Sentry on expected network errors (err.message.includes('Failed to fetch')) and user-config errors (err.isUserError), but had no way to recognize this mailsync-originated network failure, so it reported it every time.

The fix

app/src/mailsync-process.ts:

  • _spawnAndWait's close listener now captures signal in addition to code, so any future non-network crash includes the signal in its message instead of a bare null.
  • Added _buildCrashError(code, signal, rawLog): detects the "offline":true marker mailsync logs for this class of exception and, when present, builds a friendly, localized ErrorConnection message tagged with error.isNetworkError = true instead of dumping the raw crash log as the error message.

app/internal_packages/onboarding/lib/oauth-signin-page.tsx:

  • _onError now also treats err.isNetworkError as a network error (skips AppEnv.reportError, same as the existing Failed to fetch case), and shows the friendlier "check your internet connection" message to the user instead of the raw crash dump.

This doesn't fix the underlying TLS/network condition (that's outside the app's control), but it stops non-actionable, expected connectivity failures from generating Sentry noise, and makes any real future crash in this path show the signal that killed the process instead of null.

Test plan

  • npx tsc --noEmit passes for the changed files.
  • (Manual, not verifiable in this environment) confirm the onboarding OAuth flow still surfaces its normal error UI when proc.test() rejects for other reasons (e.g. bad credentials), since LocalizedErrorStrings.ErrorConnection and the JSON-response error path are unchanged for those cases.

Generated by Claude Code

…failures

MAILSPRING-CLIENT-Q: When mailsync crashes with an uncaught C++ exception
(e.g. a libcurl/TLS failure while refreshing an OAuth token) instead of
exiting cleanly, MailsyncProcess._spawnAndWait's close handler only read
the numeric `code` param, so a signal-terminated process rendered as an
uninformative "mailsync: null" error with no indication of what happened.

Nearly all 121 occurrences (35 users, mostly mainland China) show the same
underlying signature: mailsync logs `"offline":true,"retryable":true` before
crashing while calling login.microsoftonline.com during account setup -
consistent with local network/TLS interception rather than an app bug.

Capture the `signal` param so future crashes are diagnosable, and detect
the "offline" marker to surface a friendly, localized connection error
tagged with `isNetworkError`. oauth-signin-page.tsx already skips Sentry
reporting for fetch-based network errors; extend that check to include
mailsync-originated network failures too.

Claude-Session: https://claude.ai/code/session_01Ndr7sjz8nxt9VhvGb7DueH
@indent-staging

indent-staging Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor
PR Summary

Fixes the opaque "mailsync: null" error users see when the mailsync child process is signal-terminated (e.g. an uncaught libcurl/TLS exception while refreshing OAuth tokens during account setup), and suppresses ~121 Sentry reports/35 users caused by these local network/TLS-interception issues. Both change sites in _spawnAndWait's close handler now route through a shared _buildCrashError that captures the signal param and, when the raw log contains mailsync's "offline":true marker, emits a localized network error tagged isNetworkError. The OAuth signin page's existing "skip Sentry on network errors" branch is extended to honour that tag.

  • app/src/mailsync-process.ts: _proc.on('close', ...) now captures signal, both non-JSON crash paths call a new _buildCrashError(code, signal, rawLog) helper that detects /"offline"\s*:\s*true/ and returns either Error(LocalizedErrorStrings.ErrorConnection) with isNetworkError=true or the previous "unknown error" message extended with (signal: <sig>); rawLog is preserved on the error in both branches.
  • app/internal_packages/onboarding/lib/oauth-signin-page.tsx: _onError treats err.isNetworkError as a network error in addition to err.message?.includes('Failed to fetch'), applying the same friendly localized message and skipping AppEnv.reportError.

Issues

All clear! No issues remaining. 🎉

5 issues already resolved
  • The new _buildCrashError signature on line 299 is 102 characters (over prettier's 100 printWidth) and fails prettier --check, which will break CI's npm run lint:check step; prettier wants the parameters wrapped onto individual lines. (fixed by commit cef9c5d)
  • The "offline":true heuristic in _buildCrashError runs for every _spawnAndWait crash, including migrate() and resetCache(), not just the OAuth test() path — if either ever emits an offline log line before an unrelated crash, it will be silently reclassified as a network error and skipped from Sentry, masking real bugs. (fixed by commit 510a155)
  • Non-offline crash message always renders both code and signal even though exactly one is populated, so users see "mailsync: 1 (signal: null)" or "mailsync: null (signal: SIGSEGV)"; consider conditionally including only the populated field. (fixed by commit 510a155)
  • Prettier still fails on mailsync-process.ts: the multi-line template around localized('An unknown error has occurred') (lines 310-312) was wrapped when the message included (signal: ${signal}), but now that exitDescription shortened it, the collapsed one-line form (~98 chars) fits under printWidth and prettier wants it back on a single line — so npm run lint:check will still fail. (fixed by commit 48e8c55)
  • _buildCrashError(code: number, signal: string, rawLog: string) types don't match Node's close event — code is number | null and signal is NodeJS.Signals | null (exactly the case this PR handles); it only compiles because strictNullChecks is off in app/tsconfig.json. (fixed by commit 510a155)

CI Checks

All CI checks passed on commit 48e8c55.

Custom Rules 3 rules evaluated, 3 passed, 0 failed

Passing This is a longer title to see what happens when they are too long to fit
Passing B
Passing Ben Rule

View all rules

Comment thread app/src/mailsync-process.ts Outdated
Comment thread app/src/mailsync-process.ts Outdated
Comment thread app/src/mailsync-process.ts Outdated
- _buildCrashError now takes the spawn `mode` and only treats the
  "offline":true marker as a network failure when mode === 'test', so an
  unrelated crash in migrate()/resetCache() can't be silently reclassified
  and hidden from Sentry.
- code/signal now typed as number | null / NodeJS.Signals | null to match
  what the 'close' event actually provides.
- The fallback message now shows only whichever of code/signal is populated
  instead of always printing both (avoiding "mailsync: 1 (signal: null)").
Comment thread app/src/mailsync-process.ts Outdated
Comment thread app/src/mailsync-process.ts Outdated
@bengotow
bengotow merged commit 11c70e7 into master Jul 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants