fix(ipc): recover the CEF postMessage fallback instead of dereferencing undefined (#5155) - #5277
Conversation
…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).
There was a problem hiding this comment.
M3gA-Mind has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 59 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
Comment |
Summary
window.ipc.postMessagefallback transport (app/src/utils/ipcTransportFallback.ts) so the CEF IPC fallback path can never dereferenceundefined, and so a latched fallback recovers instead of bricking the session.app/src/main.tsxbefore anything caninvoke().safeInvoke's classifier to recognise the guarded IPC-unavailable rejections (plain{ message }, not aTypeError), soinstanceof IpcUnavailableErrordegradation branches keep firing.Problem
Sentry
TAURI-REACT-6—TypeError: Cannot read properties of undefined (reading 'postMessage')insendIpcMessage, 117 events / 36 users, unhandled.The chain:
app/src-tauri/vendor/tauri-cef/crates/tauri/scripts/ipc-protocol.js) dispatches everyinvoke()over theipc://localhost/<cmd>custom protocol viafetch. When thatfetchrejects — webview teardown, a reload/navigation interrupting an in-flight request, a scheme/CSP block — it latches the module-globalcustomProtocolIpcFailed = trueand re-dispatches throughwindow.ipc.postMessage(data).window.ipcis wired by wry'swith_ipc_handler. The CEF runtime discards it —tauri-runtime-cef/src/cef_impl.rsdestructuresipc_handler: _(two sites). So on every OpenHuman desktop buildwindow.ipcisundefinedand that line throws.fetch(...).then(ok, err)rejection handler, so it escapes as an unhandled promise rejection (hence Sentry'sunhandledtag) and the originalinvoke()promise never settles — the caller hangs forever.customProtocolIpcFailedis sticky for the lifetime of the document. One transientfetchrejection therefore routes every subsequentinvoke()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 theTypeErrorbut 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__ })(seecrates/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()defineswindow.ipc.postMessagebefore React mounts. The property is therefore always a function → theundefineddereference is structurally impossible, independent of which vendored bootstrap ships.ipc://custom protocol (the only transport CEF wires) with the exactTauri-Callback/Tauri-Error/Tauri-Invoke-Key/Content-Typeheaderscrates/tauri/src/ipc/protocol.rsexpects, and routes the response back throughrunCallbackmirroring the vendored success path (Tauri-Response→ callback vs error id, content-type dispatch). A latchedcustomProtocolIpcFailedtherefore keeps serving IPC.invoke()'s Promise executor and from a.then()rejection handler, where a throw is by definition an unhandled rejection.runCallback(error, …)so callers reject instead of hanging.__TAURI_INTERNALS__is wired are bounded-queued and flushed (64 max, 50ms poll mirroringcore.js'swaitForIpc, 10s deadline then reject) rather than dropped.Design notes / tradeoffs:
tauri-cefchange; installing a working bridge on the app side fixes the same failure for any vendored revision, including already-shipped ones.payloadarrives as a number array and is re-sent asapplication/jsonrather thanapplication/octet-stream. serde still deserializes that intoVec<u8>command params, so the call succeeds — just less compact. This only ever runs on the recovery path.{ message }object, which the olderr instanceof TypeError && msg.includes('postMessage')test misses. Without the update everyinstanceof IpcUnavailableErrorgraceful-degradation branch would silently go dead. The error now also preserves the real reason instead of collapsing to'IPC bridge not wired'.Submission Checklist
app/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 incommon.test.ts. 42 passing.ipcTransportFallback.ts(dispatch, both guard arms, both response routes, both queue exits) has a test.N/A: behaviour-only fix, no feature row added/removed/renamed## Related—N/A: no matrix feature IDs affectedN/A: no new deps; the fallback reuses the existing in-process ipc:// custom protocol, and tests stub global fetchN/A: no new user-facing surface; existing IPC smoke coverage applies unchangedCloses #NNNin the## RelatedsectionImpact
ipc://origin — no new capability, no new origin, no credential handling.window.ipcis defined non-enumerable.Object.definePropertyat boot. The queue only exists during a bootstrap gap and is bounded (64 entries / 10s).window.ipcalready present). Independent of the vendoredtauri-cefrevision.TAURI-REACT-6to zero and remove the associated hung-invoke()class of "silent no-op" reports.Related
customProtocolIpcFailedlatch in the vendored bootstrap is still wrong upstream — it should not latch when no usable postMessage transport exists. Worth atinyhumansai/tauri-cefPR so slim/other embedders benefit too. Not required for this fix.mainpinstauri-cefat455b47deb, which is not reachable from any branch ontinyhumansai/tauri-cef(origin/feat/cefis atf09d7e746). Looks like an unpushed local submodule commit got pinned.AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
fix/5155-sendipc-postmessage-guard84453709fd99a03b208a3d735a685d3674c84fb9Validation Run
pnpm --filter openhuman-app format:check— Prettier clean on all 5 changed filespnpm typecheck—tsc --noEmit -p app/tsconfig.jsonclean; ESLint clean on all 5 filesvitest run src/utils/ipcTransportFallback.test.ts src/utils/tauriCommands/common.test.ts→ 2 files, 42 passedN/A: no Rust changedN/A: no Tauri shell changedValidation Blocked
command:N/Aerror:N/Aimpact:N/ABehavior Changes
ipc://failure the app keeps working instead of every subsequent Tauri command silently failing for the rest of the session.Parity Contract
customProtocolIpcFailedlatches). A real wry-providedwindow.ipcis left completely alone.convertFileSrc(cmd, 'ipc'), same four headers, sameTauri-Response-based callback selection, samecontent-type→json/text/arrayBufferbody dispatch (including the split on,for duplicated content-type headers).Duplicate / Superseded PR Handling
typeofguard in the vendored script; this PR is complementary (it fixes the sticky-latch behaviour that guard leaves in place), not a replacement.