[MM-68749] Guard webContents finish-load handlers against destroyed objects - #3822
Conversation
…bjects
Several `webContents.once('did-finish-load' | 'did-frame-finish-load', ...)`
handlers touched their underlying Electron object without an `isDestroyed()`
check. During app quit / popout teardown, queued finish-load events can fire
after the WebContents/BrowserWindow is destroyed, throwing
"TypeError: Object has been destroyed" (visible in Sentry shortly after a
burst of `renderer.destroyed` events).
The `did-finish-load` handlers used to register views could leak references to destroyed WebContents into the metrics maps if the event fired during teardown. The 60s metrics interval would then call `webContents.send(...)` on those entries and throw "TypeError: Object has been destroyed". Guard at registration time and lazily drop destroyed entries from the maps in `runMetrics` and `sendMetrics`.
📝 WalkthroughWalkthroughThis PR adds defensive checks throughout the Electron app to detect and safely handle destroyed windows and webContents. Event handlers and callbacks now verify that windows, webContents, and views still exist and are not destroyed before attempting to operate on them, preventing errors and stale state updates. ChangesWindow and WebContents Destruction Safety
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/views/loadingScreen.ts (1)
49-56:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
show()can still hit destroyed window via unconditionalsetBounds().The new guards are good, but
this.setBounds()at line 85 still runs unconditionally. In the loading path, it executes immediately after registering the async callback, and in both paths, a destroyed parent between the guard checks and thesetBounds()call would cause a crash when accessing the parent ingetWindowBoundaries().Guard
setBounds()before calling it to prevent accessing a destroyed parent during application teardown.🤖 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 `@src/app/views/loadingScreen.ts` around lines 49 - 56, The show method currently calls this.setBounds() unconditionally which can access a destroyed parent via getWindowBoundaries(); modify show (and the did-finish-load callback) to guard before calling this.setBounds() by checking that this.view.webContents.isDestroyed() and this.parent.isDestroyed() are false (or that both are not destroyed) and that this.state is still LoadingScreenState.VISIBLE, then only call this.setBounds(); update the code paths around the this.view.webContents.once('did-finish-load', ...) callback and the immediate path to perform these checks before invoking setBounds().
🤖 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.
Outside diff comments:
In `@src/app/views/loadingScreen.ts`:
- Around line 49-56: The show method currently calls this.setBounds()
unconditionally which can access a destroyed parent via getWindowBoundaries();
modify show (and the did-finish-load callback) to guard before calling
this.setBounds() by checking that this.view.webContents.isDestroyed() and
this.parent.isDestroyed() are false (or that both are not destroyed) and that
this.state is still LoadingScreenState.VISIBLE, then only call this.setBounds();
update the code paths around the this.view.webContents.once('did-finish-load',
...) callback and the immediate path to perform these checks before invoking
setBounds().
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: f5416a02-2708-433a-b0cd-4dc2a93740d4
📒 Files selected for processing (8)
src/app/callsWidgetWindow.tssrc/app/mainWindow/mainWindow.tssrc/app/mainWindow/modals/modalView.tssrc/app/views/MattermostWebContentsView.tssrc/app/views/loadingScreen.tssrc/app/windows/baseWindow.tssrc/app/windows/popoutManager.tssrc/main/performanceMonitor.ts
devinbinnie
left a comment
There was a problem hiding this comment.
This bit about Electron is really annoying - first off, most of the send() calls should be no-ops if the web contents is destroyed, so I don't know why it has to throw an exception for these. Second, the fact that we have to destroy them in the first place and that they won't be automatically removed is why we have to deal with this at all.
Anyways, rant over. Just one non-blocking comment.
| if (ViewManager.isPrimaryView(this.view.id)) { | ||
| this.webContentsView.webContents.send(BROWSER_HISTORY_PUSH, this.lastPath); | ||
| } else { | ||
| const pathToPush = this.lastPath; |
There was a problem hiding this comment.
Does this have to be pulled out? I don't think there's a case where this is changed between the call and the did-finish-load event.
There was a problem hiding this comment.
@devinbinnie - sorry, thought I had responded:
Yes, we need this. CodeRabbit picked this up and Opus agreed (I envision a Penguin whenever I say that). Line 292 doesn't have this issue as it is synchronous but the once('did-finish-load') is just registered, line 303 sets lastPath to undefined and some ms later, the once handler runs.
Summary
Several
webContents.once('did-finish-load' | 'did-frame-finish-load', ...)handlers in the main process touched their underlying Electron object inside the callback body without checkingisDestroyed()first. Duringapp.quit(and other teardown races such as removing a popout/server view) a queued finish-load event can fire after theWebContents/BrowserWindowis destroyed; the handler then callssend,focus,setTitle,show,getURL,loadURL, setszoomLevel, etc., and Electron throwsTypeError: Object has been destroyed.This shows up in Sentry shortly after a burst of
renderer.destroyedevents. Representative breadcrumb timeline from the linked Sentry issue:/api/v4/system/ping,/api/v4/config/client)window.closedx2,app.quitrenderer.destroyedTypeError: Object has been destroyedThe non-Mattermost server is incidental: the partial-load state just makes pending finish-load events more likely. The same handlers can fire on any quit while a popout/loading screen is mid-load.
This PR applies the same
isDestroyed()guard pattern already used elsewhere in the codebase (e.g.webContentsManager.sendToAll,tray.update,MattermostWebContentsView.loadRetry) to:Commit 1 —
did-finish-load/did-frame-finish-loadhandler guards:BaseWindow— before settingwebContents.zoomLevelPopoutManager.startPopoutWindow+handleViewUpdated— beforeshow/setTitle/sendLoadingScreen.show+fade— beforesend/addChildViewMattermostWebContentsView.useLastPath— beforesend(BROWSER_HISTORY_PUSH)(also captureslastPathin a local since the field is cleared synchronously afterreload())ModalView.show— beforefocus()MainWindow— extends existing!this.wincheck withisDestroyed()CallsWidgetWindow—did-frame-finish-loadgetURL()was outside the existingtry/catchCommit 2 —
PerformanceMonitordefense-in-depth:PerformanceMonitor.registerView/registerServerViewusewebContents.on('did-finish-load', …)to insert into the metrics maps. If this fires during teardown, the maps end up with a destroyedWebContentsreference; the 60srunMetrics/sendMetricsinterval would then callsend(...)on it. Skip insertion at registration time and lazily drop destroyed entries when iterating.Ticket Link
https://mattermost.atlassian.net/browse/MM-68749 (Sentry: https://mattermost-mr.sentry.io/issues/7393437427/)
Checklist
[ ] Has UI changesnpm run lint:jsfor proper code formattingE2E/RunDevice Information
N/A — defensive guards in main-process code paths.
Release Note
Change Impact: 🟠 Medium
Regression Risk: Changes add defensive guards across multiple window/view modules and the PerformanceMonitor. While they are narrowly-scoped (preventing accesses to destroyed Electron objects) and avoid altering public APIs or core business logic, the modifications span several modules (BaseWindow, PopoutManager, LoadingScreen, ModalView, MainWindow, CallsWidgetWindow, MattermostWebContentsView, PerformanceMonitor) and adjust lifecycle behaviors that can affect teardown and app-quit flows. Unit tests were added for key areas (PerformanceMonitor, MattermostWebContentsView, and several view/window tests) but some guarded paths remain simple and could lack exhaustive tests. Overall, moderate risk of regressions in teardown/finish-load sequencing or metrics reporting if edge cases are missed.
QA Recommendation: Perform targeted manual QA on app quit and popout/teardown flows, including:
Given existing automated tests cover many changes, a focused manual test pass is recommended rather than full regression.
Generated by CodeRabbitAI