🐛 Fix fetch abort errors escaping the unhandled exception handler (#8801)

When AbortController.abort(reason) is called with a custom reason (a
ClojureScript ExceptionInfo), modern browsers (Chrome 98+, Firefox 97+)
reject the fetch promise with that reason object directly instead of with
the canonical DOMException{name:'AbortError'}.  The ExceptionInfo has
.name === 'Error', so both the p/catch guard and is-ignorable-exception?
failed to recognise it as an abort, letting it surface to users as an
error toast.

Fix by calling .abort() without a reason so the browser always produces
a native DOMException whose .name is 'AbortError', which is correctly
handled by all existing guards.

Also add a defense-in-depth check in is-ignorable-exception? that
filters errors whose message matches the 'fetch to \'' prefix, guarding
against any future re-introduction of a custom abort reason.

Co-authored-by: Penpot Dev <dev@penpot.app>
This commit is contained in:
Andrey Antukh
2026-03-26 14:13:38 +01:00
committed by GitHub
parent 1a4ca6d04b
commit 3eaf67a385
2 changed files with 16 additions and 9 deletions

View File

@@ -355,10 +355,11 @@
(= message "Unexpected end of input")
(str/starts-with? message "invalid props on component")
(str/starts-with? message "Unexpected token ")
;; Abort errors are expected when an in-flight HTTP request is
;; cancelled (e.g. via RxJS unsubscription / take-until). They
;; are handled gracefully inside app.util.http/fetch and must
;; NOT be surfaced as application errors.
;; Native AbortError DOMException: raised when an in-flight
;; HTTP fetch is cancelled via AbortController (e.g. by an
;; RxJS unsubscription / take-until chain). These are
;; handled gracefully inside app.util.http/fetch and must NOT
;; be surfaced as application errors.
(= (.-name ^js cause) "AbortError"))))
(on-unhandled-error [event]

View File

@@ -127,11 +127,17 @@
(fn []
(vreset! unsubscribed? true)
(when @abortable?
;; Provide an explicit reason so that the resulting AbortError carries
;; a meaningful message instead of the browser default
;; "signal is aborted without reason".
(.abort ^js controller (ex-info (str "fetch to '" uri "' is aborted")
{:uri uri}))))))))
;; Do NOT pass a custom reason to .abort(): browsers that support
;; AbortController reason (Chrome 98+, Firefox 97+) would reject
;; the fetch promise with the supplied value directly. When that
;; value is a ClojureScript ExceptionInfo its `.name` property is
;; "Error", not "AbortError", which defeats every existing guard
;; that checks `(= (.-name cause) "AbortError")`. Calling .abort
;; without a reason always produces a native DOMException whose
;; `.name` is "AbortError", which is correctly recognised and
;; suppressed by both the p/catch handler and the global
;; unhandled-exception filter.
(.abort ^js controller)))))))
(defn response->map
[response]