fix(app): guard CEF IPC fallback sync throw via safeInvoke wrapper (Sentry TAURI-REACT-7 + 6)#2619
Conversation
Adds `safeInvoke<T>()` + `IpcUnavailableError` in `tauriCommands/common.ts` that wrap `@tauri-apps/api/core::invoke()` in a try/catch so a synchronous `TypeError: Cannot read properties of undefined (reading 'postMessage')` — raised by the vendored CEF IPC fallback when `window.ipc` is unwired — is converted into a rejected Promise instead of escaping the executor and landing on `onunhandledrejection`. The classifier surfaces the specific CEF failure as `IpcUnavailableError` so call sites can `.catch((e) => e instanceof IpcUnavailableError ? …)` and degrade gracefully; unrelated throws pass through verbatim so the existing message-based classifiers (e.g. `classifyWebviewAccountError`) keep matching. Adds 8 Vitest cases covering: resolved value, 1/2/3-arg arity preservation, async rejection passthrough, sync throw → rejected Promise, CEF TypeError wrapping (sync + async paths), unrelated TypeError passthrough. Sentry-Issue: TAURI-REACT-7 Sentry-Issue: TAURI-REACT-6
Routes the remaining `@tauri-apps/api/core::invoke()` call-sites that
the Sentry TAURI-REACT-7 / TAURI-REACT-6 root-cause investigation
flagged as unguarded (sync throw escapes Promise executor → unhandled
rejection) through the `safeInvoke` wrapper introduced in the previous
commit.
Migrated via `import { safeInvoke as invoke } from …/common` so the
existing call expressions stay verbatim; semantics change only for the
sync-throw path (now a rejected Promise that `.catch(...)` chains can
discriminate via `IpcUnavailableError`).
Files migrated:
- services/webviewAccountService.ts (largest cluster — every Tauri IPC
surface in the webview-account lifecycle: scan, hide, reveal, focus,
delete, get-html, save-cookie, restore-cookie, etc.)
- services/coreProcessControl.ts (core process lifecycle invokes)
- lib/nativeNotifications/tauriBridge.ts (native-notification surface)
- utils/tauriCommands/auth.ts (auth / token bridge)
- utils/tauriCommands/conscious.ts (conscious helper)
- components/settings/panels/{DeveloperOptionsPanel,McpServerPanel}.tsx
- overlay/OverlayApp.tsx (overlay window lifecycle)
Tests:
- services/__tests__/coreProcessControl.test.ts updated to stub
`safeInvoke` instead of bare `invoke`; suite stays green (10/10).
- components/settings/panels/McpServerPanel.test.tsx same treatment
(4/4 passing).
Vendor patch (window.ipc?.postMessage?.() guard in tauri-cef
submodule) deferred — app-side `safeInvoke` alone closes the leak;
the vendor change needs submodule push access and a coordinated pin
bump; tracked as follow-up in the PR body.
Sentry-Issue: TAURI-REACT-7
Sentry-Issue: TAURI-REACT-6
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR introduces safeInvoke, a wrapper around Tauri's IPC invoke that converts synchronous CEF throws into rejected Promises, adds IpcUnavailableError to classify a postMessage bridge failure, and updates callers and tests across multiple components and services to use the wrapper. ChangesIPC Safety Wrapper Rollout
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
app/src/components/settings/panels/McpServerPanel.test.tsx (1)
19-19: 💤 Low valueConsider removing the unused
@tauri-apps/api/coremock.Since
McpServerPanelnow importssafeInvokefromtauriCommands/common(line 9 of the component), and the mock at lines 25–28 intercepts that import and forwards directly tohoisted.invoke, the mock at line 19 for@tauri-apps/api/coreis no longer called. The realinvokefrom@tauri-apps/api/coreis bypassed entirely by thesafeInvokemock.Removing this line would simplify the test setup without affecting behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/components/settings/panels/McpServerPanel.test.tsx` at line 19, The test currently includes an unused mock for '`@tauri-apps/api/core`' that is bypassed by the existing mock of safeInvoke (from tauriCommands/common) which forwards to hoisted.invoke; remove the vi.mock('`@tauri-apps/api/core`', () => ({ invoke: hoisted.invoke })); line in McpServerPanel.test.tsx so the test setup is simplified and only the safeInvoke/mock for tauriCommands/common (and hoisted.invoke) remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/utils/tauriCommands/auth.ts`:
- Line 10: The import currently brings in CommandResponse at runtime; change it
to a type-only import so the compiler/tree-shaker knows it's only a type.
Replace the single import from './common' with a type-only import for
CommandResponse (import type { CommandResponse }) and keep runtime imports for
safeInvoke (aliased to invoke) and isTauri as normal (import { safeInvoke as
invoke, isTauri }), updating the import statement in auth.ts where
CommandResponse, safeInvoke, and isTauri are referenced.
---
Nitpick comments:
In `@app/src/components/settings/panels/McpServerPanel.test.tsx`:
- Line 19: The test currently includes an unused mock for '`@tauri-apps/api/core`'
that is bypassed by the existing mock of safeInvoke (from tauriCommands/common)
which forwards to hoisted.invoke; remove the vi.mock('`@tauri-apps/api/core`', ()
=> ({ invoke: hoisted.invoke })); line in McpServerPanel.test.tsx so the test
setup is simplified and only the safeInvoke/mock for tauriCommands/common (and
hoisted.invoke) remains.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 84883eee-d213-4b53-aa3e-3f3c167a3eae
📒 Files selected for processing (12)
app/src/components/settings/panels/DeveloperOptionsPanel.tsxapp/src/components/settings/panels/McpServerPanel.test.tsxapp/src/components/settings/panels/McpServerPanel.tsxapp/src/lib/nativeNotifications/tauriBridge.tsapp/src/overlay/OverlayApp.tsxapp/src/services/__tests__/coreProcessControl.test.tsapp/src/services/coreProcessControl.tsapp/src/services/webviewAccountService.tsapp/src/utils/tauriCommands/auth.tsapp/src/utils/tauriCommands/common.test.tsapp/src/utils/tauriCommands/common.tsapp/src/utils/tauriCommands/conscious.ts
- auth.ts: use inline `type` modifier on CommandResponse so the compiler/tree-shaker knows it's type-only, while keeping `safeInvoke as invoke` + `isTauri` as runtime imports in a single statement (avoids no-duplicate-imports). - McpServerPanel.test.tsx: drop the redundant `@tauri-apps/api/core` mock — the `safeInvoke` mock at the same hoisted.invoke target shadows it for every panel call site. No behavior change. 30/30 in focused vitest pass.
CI statusCodeRabbit approved after the inline
The PR's own surface — |
Summary
safeInvoke<T>()+IpcUnavailableErrorinapp/src/utils/tauriCommands/common.tsto convert synchronous CEF IPC throws into rejected Promises so callers can.catch(...)instead of leaking toonunhandledrejection.invoke()call sites acrosswebviewAccountService.ts,coreProcessControl.ts,tauriBridge.ts,auth.ts,conscious.ts, two Settings panels, andOverlayApp.tsxto route through the wrapper via ansafeInvoke as invokeimport alias.TAURI-REACT-7(12 ev) +TAURI-REACT-6(10 ev) —TypeError: Cannot read properties of undefined (reading 'postMessage').tauri-cefsubmodule deferred (see Follow-up TODOs).Problem
When the vendored CEF IPC primary-protocol fetch rejects mid-session (network blip, navigation interrupt, etc.),
app/src-tauri/vendor/tauri-cef/crates/tauri/scripts/ipc-protocol.js:66setscustomProtocolIpcFailed = truepermanently. Every subsequentinvoke()then takes the fallbackelsebranch at line 84 and callswindow.ipc.postMessage(data)— butwindow.ipcis never wired on CEF (tauri-runtime-cef/src/cef_impl.rs:3440,3952both dropipc_handler: _). The bare access synchronously throwsTypeError: Cannot read properties of undefined (reading 'postMessage'), which escapes thenew Promise(executor)body as an unhandled rejection (not a normal Promise rejection — call-site.catch(...)never fires).22 events across the two Sentry issues, 0 user count — likely 1 Windows CEF profile that lost its custom-protocol channel mid-session and then crashed N IPC calls in the same document lifetime before reload.
Two compounding gaps:
@tauri-apps/api/core::invoke()call site in the app is unguarded against sync throws — the typical.catch(...)chain doesn't help.customProtocolIpcFailedis set, it stays set for the document lifetime.This PR fixes gap (1). Gap (2) is a vendor-side change tracked as a follow-up (see Related).
Solution
Commit 1 —
fix(app): introduce safeInvoke wrapper to catch CEF IPC sync throwsNew helper
safeInvoke<T>(cmd, args?, options?)inapp/src/utils/tauriCommands/common.tswraps@tauri-apps/api/core::invoke()in try/catch. The specific CEFTypeError: Cannot read properties of undefined (reading 'postMessage')shape is surfaced as a typedIpcUnavailableErrorso call sites can discriminate; unrelated throws pass through verbatim so existing message-based classifiers (e.g.classifyWebviewAccountError) keep matching.8 Vitest cases pin the contract: resolved value, 1/2/3-arg arity preservation, async rejection passthrough, sync throw → rejected Promise, CEF TypeError wrapping on both sync + async paths, unrelated TypeError passthrough.
Commit 2 —
refactor(app): migrate unguarded invoke() call-sites to safeInvokeMigrated via
import { safeInvoke as invoke } from '…/common'— existing call expressions stay verbatim; semantics change only for the sync-throw path. Files:services/webviewAccountService.ts— largest cluster (~14 webview-account lifecycle IPC sites: scan, hide, reveal, focus, delete, get-html, save-cookie, restore-cookie, …)services/coreProcessControl.ts— core process lifecycle invokeslib/nativeNotifications/tauriBridge.ts— native-notification surfaceutils/tauriCommands/auth.ts— auth / token bridgeutils/tauriCommands/conscious.ts— conscious helpercomponents/settings/panels/DeveloperOptionsPanel.tsx,McpServerPanel.tsxoverlay/OverlayApp.tsx— overlay window lifecycleTests updated:
services/__tests__/coreProcessControl.test.ts(10/10 passing)components/settings/panels/McpServerPanel.test.tsx(4/4 passing)Rejected alternatives
ipc-protocol.jsonly: would recover the channel for future calls in the same session but doesn't catch the sync throw at the boundary the app sees today. Also requires a coordinated submodule push + pin bump. Defer it to a follow-up.invoke()import in the app: out of scope. Only the call sites Phase 2 flagged as unguarded against sync throws got migrated. Other invoke paths (deep-link plugin, etc.) already have their own try/catch.Submission Checklist
diff-cover) meet the gate enforced by.github/workflows/coverage.yml. Runpnpm test:coverageandpnpm test:rustlocally; PRs below 80% on changed lines will not merge.sentry.tinyhumans.aiissuesTAURI-REACT-7+TAURI-REACT-6); no GitHub issue to close.Sentry-Issue:headers in## Relatedso the post-merge resolver hook can flip them.Impact
app/src/utils/tauriCommands/,app/src/services/, etc.). Desktop only. CEF runtime is where the sync-throw bug surfaces; the wrapper is a no-op on wry-backed runtimes (sync path never throws).invoke()call; never on the happy path).safeInvoke as invokeimport alias keeps every existing call expression unchanged; semantics change only on a path that previously crashed.Related
app/src-tauri/vendor/tauri-cef/crates/tauri/scripts/ipc-protocol.js:70-84— guardwindow.ipc?.postMessage?.()+ resetcustomProtocolIpcFailed = falseafter fallback failure so the next invoke retries custom-protocol. Defensive layer that recovers the channel mid-session. Requires submodule push + parent pin bump; tracked separately to keep this PR scoped.Sentry-Issue: TAURI-REACT-7
Sentry-Issue: TAURI-REACT-6
AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
fix/sentry-react-7-6-postmessage-guard681656b4(tip — 2 micro-commits ahead ofupstream/main@9a65e863)Validation Run
pnpm compile(tsc --noEmit) — clean.pnpm lint— 0 errors / 60 pre-existing warnings (not on changed code). Focused Vitest oncommon.test.ts+coreProcessControl.test.ts+McpServerPanel.test.tsx— 32/32 passing.app/only).app/src-tauri/source not touched.Validation Blocked
command:N/Aerror:N/Aimpact:N/ABehavior Changes
@tauri-apps/api/core::invoke()(specifically the CEF IPC fallbackTypeError: Cannot read properties of undefined (reading 'postMessage')shape, and any other sync throw in the same channel) are now converted into rejected Promises. Existing.catch(...)chains start handling these instead of escaping toonunhandledrejection.Parity Contract
invoke()'s contract; failure modes that previously rejected normally still reject with the original error.isTauri()callers that previously short-circuited the call still do (the wrapper does not gate on environment — it only converts sync throws). Tests pin no-throw, classified throw, unclassified throw, and async rejection passthrough.Pre-existing main CI failures (NOT introduced by this PR)
For reviewer convenience: the 3 frontend jobs (
Frontend Coverage,test / Frontend Unit Tests,test / i18n Coverage) are red onmainsince 2026-05-21 (PR #2378 added German locale support without backfilling the 20 keys PR #2280 added for the MCP Server panel —coverage.test.ts:77strict-equality fails ondemissing keys). This PR touches zero locale files and inherits the failure from the merge base. Precedent: PR #2481 merged through the same state by maintainer ack. See #2481 (comment).Summary by CodeRabbit
Refactor
Bug Fixes
Tests