Improve error handling and exception formatting (#8757)

*  Improve error handling and exception formatting

- Enhance exception formatting with visual separators and cause chaining
- Add new handler for :internal error type
- Refine error types: change assertion-related errors to :assertion type
- Improve error messages and hints consistency
- Clean up error handling in zip utilities and HTTP modules

* 🐛 Properly handle AbortError on fetch request unsubscription

When a fetch request in-flight is cancelled due to RxJS unsubscription
(e.g. navigating away from the workspace while thumbnail loads are
pending), the AbortController.abort() call triggers a catch handler
that previously relied solely on a @unsubscribed? flag to suppress the
error.

This was unreliable: nested observables spawned inside rx/mapcat (such
as datauri->blob-uri conversions within get-file-object-thumbnails)
could abort independently, with their own AbortController instances,
meaning the outer unsubscribed? flag was never set and the AbortError
propagated as an unhandled exception.

Add an explicit AbortError name check as a disjunctive condition so
that abort errors originating from any observable in the chain are
suppressed at the source, regardless of subscription state.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Andrey Antukh
2026-03-24 19:54:05 +01:00
parent cc73a768d5
commit 0dfac801a4
8 changed files with 40 additions and 26 deletions

View File

@@ -245,8 +245,10 @@
(defn format-throwable
[cause & {:as opts}]
(with-out-str
(println "====================")
(when-let [exdata (ex-data cause)]
(when-let [hint (get exdata :hint)]
(when-let [hint (or (get exdata :hint)
(ex-message cause))]
(when (str/index-of hint "\n")
(println "Hint:")
(println "--------------------")
@@ -273,7 +275,9 @@
(when-let [trace (.-stack cause)]
(println "Trace:")
(println "--------------------")
(println (.-stack cause))))))
(println (.-stack cause)))
(println "===================="))))
(defn first-line
[s]
@@ -297,6 +301,11 @@
(js/console.group title)
(try
(js/console.log (format-throwable cause))
(loop [cause (ex-cause cause)]
(when cause
(js/console.log "\nCaused by:")
(js/console.log (format-throwable cause))
(recur (ex-cause cause))))
(finally
(js/console.groupEnd))))))

View File

@@ -178,7 +178,7 @@
(rx/map (fn [{:keys [redirect-uri] :as rsp}]
(if redirect-uri
(rt/nav-raw :uri redirect-uri)
(ex/raise :type :internal
(ex/raise :type :assertion
:code :unexpected-response
:hint "unexpected response from OIDC method"
:resp (pr-str rsp)))))

View File

@@ -118,6 +118,12 @@
:level :error
:timeout 5000})))
(defmethod ptk/handle-error :internal
[error]
(st/emit! (rt/assign-exception error))
(when-let [cause (::instance error)]
(ex/print-throwable cause :prefix "Internal Error")))
(defmethod ptk/handle-error :default
[error]
(if (and (string? (:hint error))
@@ -209,8 +215,7 @@
(st/async-emit! (rt/assign-exception error))))
;; This is a pure frontend error that can be caused by an active
;; assertion (assertion that is preserved on production builds). From
;; the user perspective this should be treated as internal error.
;; assertion (assertion that is preserved on production builds).
(defmethod ptk/handle-error :assertion
[error]
(when-let [cause (::instance error)]

View File

@@ -57,13 +57,13 @@
:else
(rx/throw
(ex-info "repository request error"
{:type :internal
:code :repository-access-error
(ex/error :type :internal
:code :unable-to-process-repository-response
:hint "unable to process repository response"
:uri uri
:status status
:headers headers
:data body}))))
:data body))))
(def default-options
{:update-file {:query-params [:id]}
@@ -156,11 +156,11 @@
tpoint (ct/tpoint-ms)]
(when (and response-stream? (not stream?))
(ex/raise :type :internal
:code :invalid-response-processing
(ex/raise :type :assertion
:code :unexpected-response
:hint "expected normal response, received sse stream"
:response-uri (:uri response)
:response-status (:status response)))
:uri (:uri response)
:status (:status response)))
(if response-stream?
(-> (sse/create-stream body)

View File

@@ -44,8 +44,8 @@
(rx/end! subs)))
(obj/set! image "crossOrigin" "anonymous")
(obj/set! image "onerror" #(rx/error! subs %))
(obj/set! image "onabort" #(rx/error! subs (ex/error :type :internal
:code :abort
(obj/set! image "onabort" #(rx/error! subs (ex/error :type :abort
:code :operation-aborted
:hint "operation aborted")))
(obj/set! image "src" uri)
(fn []

View File

@@ -106,10 +106,10 @@
(p/catch
(fn [cause]
(vreset! abortable? false)
(when-not @unsubscribed?
(when-not (or @unsubscribed? (= (.-name ^js cause) "AbortError"))
(let [error (ex-info (ex-message cause)
{:type :internal
:code :unable-to-fetch
:code :fetch-error
:hint "unable to perform fetch operation"
:uri uri
:headers headers}

View File

@@ -42,8 +42,8 @@
(obj/set! reader "onerror"
#(rx/error! subs %))
(obj/set! reader "onabort"
#(rx/error! subs (ex/error :type :internal
:code :abort
#(rx/error! subs (ex/error :type :abort
:code :operation-aborted
:hint "operation aborted")))
(f reader)
(fn []

View File

@@ -8,6 +8,7 @@
"Helpers for make zip file."
(:require
["@zip.js/zip.js" :as zip]
[app.common.exceptions :as ex]
[app.util.array :as array]
[promesa.core :as p]))
@@ -27,9 +28,9 @@
(reader (js/Uint8Array. blob))
:else
(throw (ex-info "invalid arguments"
{:type :internal
:code :invalid-type}))))
(ex/raise :type :assertion
:coce :invalid-type
:hint "invalid data received for zip/reader")))
(defn blob-writer
[& {:keys [mtype]}]
@@ -62,10 +63,9 @@
(.add writer path (new zip/TextReader content))
:else
(throw (ex-info "invalid arguments"
{:type :internal
:code :invalid-type}))))
(ex/raise :type :assertion
:code :invalid-type
:hint "invalid data received for zip/add fn")))
(defn get-entry
[reader path]