penpot/frontend/src/app/main/ui/error_boundary.cljs
Andrey Antukh 5f21ebd08d
🐛 Filter ignorable React removeChild errors at error boundary and fix HTML anti-pattern (#10145)
* 🐛 Filter ignorable exceptions in error-boundary onError callback

The global uncaught-error-handler already skips NotFoundError/removeChild
and other harmless errors, but react-error-boundary's onError callback fires
independently of the window.onerror pipeline. This means the error boundary
was still logging these errors and setting last-exception, causing them to
continue appearing in error reports despite being non-actionable.

Add the is-ignorable-exception? check to the error-boundary* onError so
harmless errors are silently ignored, matching the behavior of the global
handler.

* 🐛 Fix dangerouslySetInnerHTML anti-pattern in context-notification

The previous code used dangerouslySetInnerHTML on the same element that
could also contain React children. This is a React anti-pattern that can
cause reconciliation mismatches and lead to removeChild DOMExceptions.

Refactor to use two separate element branches: one for raw HTML injection
and one for normal React children with links.
2026-06-11 16:59:00 +02:00

68 lines
2.8 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.main.ui.error-boundary
"React error boundary components"
(:require
["react-error-boundary" :as reb]
[app.common.exceptions :as ex]
[app.config :as cf]
[app.main.errors :as errors]
[app.main.refs :as refs]
[goog.functions :as gfn]
[rumext.v2 :as mf]))
(mf/defc error-boundary*
[{:keys [fallback children]}]
(let [fallback-wrapper
(mf/with-memo [fallback]
(mf/fnc fallback-wrapper*
[{:keys [error reset-error-boundary]}]
(let [route (mf/deref refs/route)
data (errors/exception->error-data error)]
[:> fallback {:data data
:route route
:on-reset reset-error-boundary}])))
on-error
(mf/with-memo []
;; NOTE: The debounce is necessary just for simplicity,
;; becuase for some reasons the error is reported twice in a
;; very small amount of time, so we debounce for 100ms for
;; avoid duplicate and redundant reports
(gfn/debounce (fn [error info]
;; If the error is a stale-asset error (cross-build
;; module mismatch), force a hard page reload instead
;; of showing the error page to the user.
(cond
(errors/stale-asset-error? error)
(cf/throttled-reload :reason (ex-message error))
;; If the error is known to be harmless (browser
;; extensions, React DOM conflicts, etc.), ignore it
;; silently — the global uncaught-error-handler
;; already does this, but react-error-boundary's
;; onError fires independently of the window.onerror
;; pipeline, so we must also filter here.
(errors/is-ignorable-exception? error)
nil
:else
(do
(set! errors/last-exception error)
(ex/print-throwable error)
(js/console.error
"Component trace: \n"
(unchecked-get info "componentStack")
"\n"
error))))
100))]
[:> reb/ErrorBoundary
{:FallbackComponent fallback-wrapper
:onError on-error}
children]))