Skip to content

fix(app): guard CEF IPC fallback sync throw via safeInvoke wrapper (Sentry TAURI-REACT-7 + 6)#2619

Merged
senamakel merged 3 commits into
tinyhumansai:mainfrom
oxoxDev:fix/sentry-react-7-6-postmessage-guard
May 25, 2026
Merged

fix(app): guard CEF IPC fallback sync throw via safeInvoke wrapper (Sentry TAURI-REACT-7 + 6)#2619
senamakel merged 3 commits into
tinyhumansai:mainfrom
oxoxDev:fix/sentry-react-7-6-postmessage-guard

Conversation

@oxoxDev
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev commented May 25, 2026

Summary

  • Add safeInvoke<T>() + IpcUnavailableError in app/src/utils/tauriCommands/common.ts to convert synchronous CEF IPC throws into rejected Promises so callers can .catch(...) instead of leaking to onunhandledrejection.
  • Migrate ~14 unguarded invoke() call sites across webviewAccountService.ts, coreProcessControl.ts, tauriBridge.ts, auth.ts, conscious.ts, two Settings panels, and OverlayApp.tsx to route through the wrapper via an safeInvoke as invoke import alias.
  • Closes the leak behind Sentry TAURI-REACT-7 (12 ev) + TAURI-REACT-6 (10 ev) — TypeError: Cannot read properties of undefined (reading 'postMessage').
  • Vendor patch on the tauri-cef submodule 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:66 sets customProtocolIpcFailed = true permanently. Every subsequent invoke() then takes the fallback else branch at line 84 and calls window.ipc.postMessage(data) — but window.ipc is never wired on CEF (tauri-runtime-cef/src/cef_impl.rs:3440,3952 both drop ipc_handler: _). The bare access synchronously throws TypeError: Cannot read properties of undefined (reading 'postMessage'), which escapes the new 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:

  1. Every @tauri-apps/api/core::invoke() call site in the app is unguarded against sync throws — the typical .catch(...) chain doesn't help.
  2. The vendored CEF fallback can't recover the channel — once customProtocolIpcFailed is 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 throws

New helper safeInvoke<T>(cmd, args?, options?) in app/src/utils/tauriCommands/common.ts wraps @tauri-apps/api/core::invoke() in try/catch. The specific CEF TypeError: Cannot read properties of undefined (reading 'postMessage') shape is surfaced as a typed IpcUnavailableError so 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 safeInvoke

Migrated 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 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.tsx, McpServerPanel.tsx
  • overlay/OverlayApp.tsx — overlay window lifecycle

Tests updated:

  • services/__tests__/coreProcessControl.test.ts (10/10 passing)
  • components/settings/panels/McpServerPanel.test.tsx (4/4 passing)

Rejected alternatives

  • Patching the vendored ipc-protocol.js only: 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.
  • Patching every 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

If a section does not apply to this change, mark the item as N/A with a one-line reason. Do not delete items.

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — changed lines (Vitest + cargo-llvm-cov merged via diff-cover) meet the gate enforced by .github/workflows/coverage.yml. Run pnpm test:coverage and pnpm test:rust locally; PRs below 80% on changed lines will not merge.
  • N/A: behaviour-only change to IPC error-shape; no product feature rows added/removed/renamed in the coverage matrix.
  • N/A: no feature IDs map to this fix — defensive IPC wrapper hygiene.
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • N/A: does not touch release-cut surfaces — IPC wrapper internal to the Tauri JS bridge.
  • N/A: Sentry-only triage (self-hosted sentry.tinyhumans.ai issues TAURI-REACT-7 + TAURI-REACT-6); no GitHub issue to close. Sentry-Issue: headers in ## Related so the post-merge resolver hook can flip them.

Impact

  • Runtime/platform: Tauri shell + frontend (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).
  • Performance: negligible (one try/catch per invoke() call; never on the happy path).
  • Security: nil (no new logging, no credential surface change).
  • Migration / compat: nil. The safeInvoke as invoke import alias keeps every existing call expression unchanged; semantics change only on a path that previously crashed.
  • Sentry impact: closes the 22-event leak on the two REACT issues. Also closes a latent class of unhandled rejections that any future CEF custom-protocol blip would surface.

Related

  • Closes:
  • Follow-up PR(s)/TODOs:
    • Vendor patch to app/src-tauri/vendor/tauri-cef/crates/tauri/scripts/ipc-protocol.js:70-84 — guard window.ipc?.postMessage?.() + reset customProtocolIpcFailed = false after 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)

Keep this section for AI-authored PRs. For human-only PRs, mark each field N/A.

Linear Issue

  • Key: N/A: triage driven from self-hosted Sentry (sentry.tinyhumans.ai) not Linear.
  • URL: N/A: see Sentry-Issue headers above.

Commit & Branch

  • Branch: fix/sentry-react-7-6-postmessage-guard
  • Commit SHA: 681656b4 (tip — 2 micro-commits ahead of upstream/main@9a65e863)

Validation Run

  • App workspace: pnpm compile (tsc --noEmit) — clean. pnpm lint — 0 errors / 60 pre-existing warnings (not on changed code). Focused Vitest on common.test.ts + coreProcessControl.test.ts + McpServerPanel.test.tsx — 32/32 passing.
  • N/A: no Rust core changes in this PR (frontend + app/ only).
  • N/A: no Tauri Rust changes; app/src-tauri/ source not touched.

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: synchronous throws from @tauri-apps/api/core::invoke() (specifically the CEF IPC fallback TypeError: 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 to onunhandledrejection.
  • User-visible effect: fewer "unknown error" toasts in webview-account lifecycle flows when CEF custom-protocol stutters; otherwise zero functional change.

Parity Contract

  • Legacy behavior preserved: every migrated call site keeps the same successful return value and same async rejection shape. The wrapper is a strict superset of invoke()'s contract; failure modes that previously rejected normally still reject with the original error.
  • Guard/fallback/dispatch parity checks: 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 on main since 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:77 strict-equality fails on de missing 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

    • Consolidated inter-process communication handling across the app for more consistent behavior.
    • Added a distinct error class to surface a specific IPC-unavailable condition.
  • Bug Fixes

    • Prevented synchronous IPC failures from causing unhandled rejections; errors now propagate as normal promise rejections.
  • Tests

    • Updated mocks and added tests to validate the new IPC wrapper and error-handling behaviors.

Review Change Stack

oxoxDev added 2 commits May 25, 2026 14:15
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
@oxoxDev oxoxDev requested a review from a team May 25, 2026 10:38
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0114cc29-01fe-4c64-a3aa-b17877edc586

📥 Commits

Reviewing files that changed from the base of the PR and between 681656b and c2e9c94.

📒 Files selected for processing (2)
  • app/src/components/settings/panels/McpServerPanel.test.tsx
  • app/src/utils/tauriCommands/auth.ts

📝 Walkthrough

Walkthrough

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

Changes

IPC Safety Wrapper Rollout

Layer / File(s) Summary
safeInvoke wrapper and IpcUnavailableError
app/src/utils/tauriCommands/common.ts
Introduces IpcUnavailableError, detection helpers for the CEF "postMessage" failure, and safeInvoke<T> which forwards to core invoke, converts synchronous throws into Promise rejections, and re-tags detected CEF failures.
safeInvoke test suite and mocking
app/src/utils/tauriCommands/common.test.ts
Extends the @tauri-apps/api/core mock to add a forwarding invoke mock and adds tests verifying argument forwarding/arity, promise rejection passthrough, sync-throw conversion, and conditional wrapping of the postMessage TypeError into IpcUnavailableError.
Settings panels and UI components switch to safeInvoke
app/src/components/settings/panels/DeveloperOptionsPanel.tsx, app/src/components/settings/panels/McpServerPanel.tsx, app/src/components/settings/panels/McpServerPanel.test.tsx
Replaces direct @tauri-apps/api/core invoke imports with safeInvoke (aliased invoke) and updates McpServerPanel test mock to provide safeInvoke forwarding to the hoisted invoke mock.
Services and utilities switch to safeInvoke
app/src/services/coreProcessControl.ts, app/src/services/webviewAccountService.ts, app/src/utils/tauriCommands/auth.ts, app/src/lib/nativeNotifications/tauriBridge.ts, app/src/overlay/OverlayApp.tsx, app/src/utils/tauriCommands/conscious.ts, app/src/services/__tests__/coreProcessControl.test.ts
Replaces direct Tauri invoke imports with safeInvoke (aliased invoke) across services and utilities; expands coreProcessControl test mock to forward safeInvoke to the hoisted invokeMock.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

bug

Suggested reviewers

  • sanil-23
  • M3gA-Mind

Poem

🐇 I hopped through code with gentle care,
Replaced raw throws with promise air,
When postMessage goes missing, oh dear,
IpcUnavailableError whispers near,
Now callers catch — no panic here.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing a safeInvoke wrapper to guard against CEF IPC fallback synchronous throws.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 25, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/src/components/settings/panels/McpServerPanel.test.tsx (1)

19-19: 💤 Low value

Consider removing the unused @tauri-apps/api/core mock.

Since McpServerPanel now imports safeInvoke from tauriCommands/common (line 9 of the component), and the mock at lines 25–28 intercepts that import and forwards directly to hoisted.invoke, the mock at line 19 for @tauri-apps/api/core is no longer called. The real invoke from @tauri-apps/api/core is bypassed entirely by the safeInvoke mock.

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

📥 Commits

Reviewing files that changed from the base of the PR and between d997394 and 681656b.

📒 Files selected for processing (12)
  • app/src/components/settings/panels/DeveloperOptionsPanel.tsx
  • app/src/components/settings/panels/McpServerPanel.test.tsx
  • app/src/components/settings/panels/McpServerPanel.tsx
  • app/src/lib/nativeNotifications/tauriBridge.ts
  • app/src/overlay/OverlayApp.tsx
  • app/src/services/__tests__/coreProcessControl.test.ts
  • app/src/services/coreProcessControl.ts
  • app/src/services/webviewAccountService.ts
  • app/src/utils/tauriCommands/auth.ts
  • app/src/utils/tauriCommands/common.test.ts
  • app/src/utils/tauriCommands/common.ts
  • app/src/utils/tauriCommands/conscious.ts

Comment thread app/src/utils/tauriCommands/auth.ts Outdated
- 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.
@coderabbitai coderabbitai Bot added the bug label May 25, 2026
@oxoxDev
Copy link
Copy Markdown
Contributor Author

oxoxDev commented May 25, 2026

CI status

CodeRabbit approved after the inline type modifier refactor — thanks. Remaining CI failures are all unrelated to the safeInvoke wrapper:

  • Rust Core Coverage (cargo-llvm-cov): vault_sync_roundtrip_updates_memory_and_ledger (integration test in tests/vault_sync_e2e.rs:101) panics. Pure Rust core; this PR's diff is TypeScript-only (app/src/utils/tauriCommands/ + a Vitest mock).
  • test / Rust Core Tests (Windows — secrets ACL): cancelled (not failed) — all 9472 tests in the run passed; the job was killed during cleanup when another job in the workflow failed first.
  • e2e / E2E (Linux / Appium Chromium): same job failing on upstream/main (run 26391619635) — inherited.

The PR's own surface — safeInvoke wrapper + 4 call sites + Vitest mock — passes test / Frontend Unit Tests (vitest) green.

@senamakel senamakel merged commit c28d9da into tinyhumansai:main May 25, 2026
29 of 35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants