Skip to content

fix(ipc): recover the CEF postMessage fallback instead of dereferencing undefined (#5155) - #5277

Merged
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/5155-sendipc-postmessage-guard
Jul 31, 2026
Merged

fix(ipc): recover the CEF postMessage fallback instead of dereferencing undefined (#5155)#5277
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/5155-sendipc-postmessage-guard

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Summary

  • Install a working window.ipc.postMessage fallback transport (app/src/utils/ipcTransportFallback.ts) so the CEF IPC fallback path can never dereference undefined, and so a latched fallback recovers instead of bricking the session.
  • Wire it in app/src/main.tsx before anything can invoke().
  • Teach safeInvoke's classifier to recognise the guarded IPC-unavailable rejections (plain { message }, not a TypeError), so instanceof IpcUnavailableError degradation branches keep firing.
  • Regression tests for both files (42 passing).

Problem

Sentry TAURI-REACT-6TypeError: Cannot read properties of undefined (reading 'postMessage') in sendIpcMessage, 117 events / 36 users, unhandled.

The chain:

  1. Tauri's vendored IPC bootstrap (app/src-tauri/vendor/tauri-cef/crates/tauri/scripts/ipc-protocol.js) dispatches every invoke() over the ipc://localhost/<cmd> custom protocol via fetch. When that fetch rejects — webview teardown, a reload/navigation interrupting an in-flight request, a scheme/CSP block — it latches the module-global customProtocolIpcFailed = true and re-dispatches through window.ipc.postMessage(data).
  2. window.ipc is wired by wry's with_ipc_handler. The CEF runtime discards it — tauri-runtime-cef/src/cef_impl.rs destructures ipc_handler: _ (two sites). So on every OpenHuman desktop build window.ipc is undefined and that line throws.
  3. The throw happens inside the fetch(...).then(ok, err) rejection handler, so it escapes as an unhandled promise rejection (hence Sentry's unhandled tag) and the original invoke() promise never settles — the caller hangs forever.
  4. customProtocolIpcFailed is sticky for the lifetime of the document. One transient fetch rejection therefore routes every subsequent invoke() down the dead branch. That explains the 117-events / 36-users ratio: a single blip bricks a session, then every command in it fails.

The typeof window.ipc.postMessage === 'function' guard added in #5171 stops the TypeError but leaves step 4 fully intact — IPC stays dead for the rest of the session, now silently. That is the part this PR fixes.

Solution

The postMessage branch hands the bridge JSON.stringify({ cmd, callback, error, options, payload, __TAURI_INVOKE_KEY__ }) (see crates/tauri/scripts/process-ipc-message-fn.js) — i.e. the envelope already carries everything the custom-protocol request needs, including the per-launch invoke key. So the fallback can be a real transport rather than a stub:

  • installIpcTransportFallback() defines window.ipc.postMessage before React mounts. The property is therefore always a function → the undefined dereference is structurally impossible, independent of which vendored bootstrap ships.
  • The implementation re-issues the request over the ipc:// custom protocol (the only transport CEF wires) with the exact Tauri-Callback / Tauri-Error / Tauri-Invoke-Key / Content-Type headers crates/tauri/src/ipc/protocol.rs expects, and routes the response back through runCallback mirroring the vendored success path (Tauri-Response → callback vs error id, content-type dispatch). A latched customProtocolIpcFailed therefore keeps serving IPC.
  • It never throws — it is called synchronously from invoke()'s Promise executor and from a .then() rejection handler, where a throw is by definition an unhandled rejection.
  • A genuinely failed request settles the pending promise via runCallback(error, …) so callers reject instead of hanging.
  • Messages arriving before __TAURI_INTERNALS__ is wired are bounded-queued and flushed (64 max, 50ms poll mirroring core.js's waitForIpc, 10s deadline then reject) rather than dropped.
  • No-op when a real wry bridge already exists; idempotent; descriptor stays writable/configurable so a later real bridge can replace it.

Design notes / tradeoffs:

  • Frontend-only, no submodule bump. The latch lives in the vendored script, but guarding it there would require a tauri-cef change; installing a working bridge on the app side fixes the same failure for any vendored revision, including already-shipped ones.
  • Fidelity caveat (documented in-file): the envelope has already been JSON round-tripped, so a binary payload arrives as a number array and is re-sent as application/json rather than application/octet-stream. serde still deserializes that into Vec<u8> command params, so the call succeeds — just less compact. This only ever runs on the recovery path.
  • Classifier update is load-bearing, not cosmetic: the guarded paths reject with a plain { message } object, which the old err instanceof TypeError && msg.includes('postMessage') test misses. Without the update every instanceof IpcUnavailableError graceful-degradation branch would silently go dead. The error now also preserves the real reason instead of collapsing to 'IPC bridge not wired'.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategyapp/src/utils/ipcTransportFallback.test.ts (18 cases: install/idempotence, custom-protocol re-dispatch + headers, ok/error routing, fetch rejection, malformed envelope, non-string payload, absent internals, queue flush, queue give-up) plus 6 added classifier cases in common.test.ts. 42 passing.
  • Diff coverage ≥ 80% — changed lines are frontend-only and covered by the two Vitest suites above; every branch in ipcTransportFallback.ts (dispatch, both guard arms, both response routes, both queue exits) has a test.
  • Coverage matrix updated — N/A: behaviour-only fix, no feature row added/removed/renamed
  • All affected feature IDs from the matrix are listed under ## RelatedN/A: no matrix feature IDs affected
  • No new external network dependencies introduced — N/A: no new deps; the fallback reuses the existing in-process ipc:// custom protocol, and tests stub global fetch
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: no new user-facing surface; existing IPC smoke coverage applies unchanged
  • Linked issue closed via Closes #NNN in the ## Related section

Impact

  • Platform: desktop (all three OSes) — the CEF webview IPC path. No mobile/web/CLI surface touched. Rust untouched.
  • Behaviour: strictly a recovery improvement. On the happy path (custom protocol working) nothing changes — the fallback is never entered. On the failure path, commands now succeed via re-dispatch instead of throwing, and genuinely unrecoverable commands reject with a descriptive error instead of hanging forever.
  • Security: the re-dispatch forwards the same invoke key the bootstrap already put in the envelope and targets the same ipc:// origin — no new capability, no new origin, no credential handling. window.ipc is defined non-enumerable.
  • Performance: one Object.defineProperty at boot. The queue only exists during a bootstrap gap and is bounded (64 entries / 10s).
  • Compatibility: no-op on a real wry runtime (window.ipc already present). Independent of the vendored tauri-cef revision.
  • Sentry: should drive TAURI-REACT-6 to zero and remove the associated hung-invoke() class of "silent no-op" reports.

Related

  • Closes: TypeError: Cannot read properties of undefined (reading 'postMessage') — sendIpcMessage #5155
  • Follow-up PR(s)/TODOs:
    • The sticky customProtocolIpcFailed latch in the vendored bootstrap is still wrong upstream — it should not latch when no usable postMessage transport exists. Worth a tinyhumansai/tauri-cef PR so slim/other embedders benefit too. Not required for this fix.
    • Unrelated but noticed while verifying: main pins tauri-cef at 455b47deb, which is not reachable from any branch on tinyhumansai/tauri-cef (origin/feat/cef is at f09d7e746). Looks like an unpushed local submodule commit got pinned.

AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/5155-sendipc-postmessage-guard
  • Commit SHA: 84453709fd99a03b208a3d735a685d3674c84fb9

Validation Run

  • pnpm --filter openhuman-app format:check — Prettier clean on all 5 changed files
  • pnpm typechecktsc --noEmit -p app/tsconfig.json clean; ESLint clean on all 5 files
  • Focused tests: vitest run src/utils/ipcTransportFallback.test.ts src/utils/tauriCommands/common.test.ts → 2 files, 42 passed
  • Rust fmt/check (if changed): N/A: no Rust changed
  • Tauri fmt/check (if changed): N/A: no Tauri shell changed

Validation Blocked

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

Behavior Changes

  • Intended behavior change: the CEF IPC postMessage fallback becomes a working transport instead of an undefined dereference; unrecoverable calls reject instead of hanging.
  • User-visible effect: after a transient ipc:// failure the app keeps working instead of every subsequent Tauri command silently failing for the rest of the session.

Parity Contract

  • Legacy behavior preserved: the happy path is untouched (the fallback is only reached once customProtocolIpcFailed latches). A real wry-provided window.ipc is left completely alone.
  • Guard/fallback/dispatch parity checks: the re-dispatch mirrors the vendored bootstrap's custom-protocol branch exactly — same URL via convertFileSrc(cmd, 'ipc'), same four headers, same Tauri-Response-based callback selection, same content-typejson/text/arrayBuffer body dispatch (including the split on , for duplicated content-type headers).

Duplicate / Superseded PR Handling

…ng undefined (tinyhumansai#5155)

Tauri's vendored IPC bootstrap dispatches every `invoke()` over the
`ipc://localhost/<cmd>` custom protocol. When that `fetch` rejects — webview
teardown, a reload interrupting an in-flight request, a scheme/CSP block — it
latches the module-global `customProtocolIpcFailed = true` and re-dispatches
through `window.ipc.postMessage(data)`.

`window.ipc` is wired by wry's `with_ipc_handler`, which the CEF runtime
discards (`tauri-runtime-cef/src/cef_impl.rs` destructures `ipc_handler: _`),
so that line throws `TypeError: Cannot read properties of undefined (reading
'postMessage')` from inside the `fetch(...).then(ok, err)` rejection handler.
That escapes as an unhandled promise rejection and leaves the `invoke()`
promise permanently pending. Because the latch is sticky for the lifetime of
the document, one transient fetch failure routes every subsequent `invoke()`
down the dead branch — one blip bricks the whole session, which is why the
issue shows 117 events across only 36 users.

A `typeof window.ipc.postMessage === 'function'` guard stops the TypeError but
keeps the session bricked. Install a working `window.ipc.postMessage` instead:
it re-dispatches the envelope over the `ipc://` custom protocol (the only
transport CEF wires) using the `cmd`/`callback`/`error`/`__TAURI_INVOKE_KEY__`
the envelope already carries, so the latched fallback recovers rather than
dies. The property is always defined, so the undefined dereference becomes
structurally impossible regardless of which vendored bootstrap ships.

- app/src/utils/ipcTransportFallback.ts — the fallback transport: never
  throws, settles the pending callback via `runCallback` on failure so callers
  reject instead of hanging, and bounded-queues messages that arrive before
  `__TAURI_INTERNALS__` is wired (64 max, 10s deadline, then reject).
- app/src/main.tsx — install it before anything can `invoke()`.
- app/src/utils/tauriCommands/common.ts — the guarded paths now reject with a
  plain `{ message }` object rather than a `TypeError`, so classify those
  messages as `IpcUnavailableError` too; otherwise every
  `instanceof IpcUnavailableError` degradation branch silently goes dead.
  Preserve the underlying reason instead of collapsing to a generic string.
- regression tests for both files (42 passing).
@M3gA-Mind
M3gA-Mind requested a review from a team July 30, 2026 13:02

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

M3gA-Mind has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 59 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9288990d-3084-4516-888b-264867824ec9

📥 Commits

Reviewing files that changed from the base of the PR and between bb83836 and 8445370.

📒 Files selected for processing (5)
  • app/src/main.tsx
  • app/src/utils/ipcTransportFallback.test.ts
  • app/src/utils/ipcTransportFallback.ts
  • app/src/utils/tauriCommands/common.test.ts
  • app/src/utils/tauriCommands/common.ts

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

@M3gA-Mind
M3gA-Mind merged commit cfc2d36 into tinyhumansai:main Jul 31, 2026
20 checks passed
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.

TypeError: Cannot read properties of undefined (reading 'postMessage') — sendIpcMessage

1 participant