From 0dfac801a4f3270d8e935d44c756f0609ad8ed9e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 24 Mar 2026 19:54:05 +0100 Subject: [PATCH] :sparkles: Improve error handling and exception formatting (#8757) * :sparkles: 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 * :bug: 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 --- common/src/app/common/exceptions.cljc | 13 +++++++++++-- frontend/src/app/main/data/auth.cljs | 2 +- frontend/src/app/main/errors.cljs | 9 +++++++-- frontend/src/app/main/repo.cljs | 16 ++++++++-------- frontend/src/app/rasterizer.cljs | 4 ++-- frontend/src/app/util/http.cljs | 4 ++-- frontend/src/app/util/webapi.cljs | 4 ++-- frontend/src/app/util/zip.cljs | 14 +++++++------- 8 files changed, 40 insertions(+), 26 deletions(-) diff --git a/common/src/app/common/exceptions.cljc b/common/src/app/common/exceptions.cljc index e104be775b..a07eda8c0e 100644 --- a/common/src/app/common/exceptions.cljc +++ b/common/src/app/common/exceptions.cljc @@ -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)))))) diff --git a/frontend/src/app/main/data/auth.cljs b/frontend/src/app/main/data/auth.cljs index a933b83590..e3aa763ad6 100644 --- a/frontend/src/app/main/data/auth.cljs +++ b/frontend/src/app/main/data/auth.cljs @@ -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))))) diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index abed794099..63e33e4aff 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -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)] diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index 0ad10286aa..ad252e2a04 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -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) diff --git a/frontend/src/app/rasterizer.cljs b/frontend/src/app/rasterizer.cljs index e2bf9ede6e..fa1e233df4 100644 --- a/frontend/src/app/rasterizer.cljs +++ b/frontend/src/app/rasterizer.cljs @@ -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 [] diff --git a/frontend/src/app/util/http.cljs b/frontend/src/app/util/http.cljs index a25f030d3f..34971caaef 100644 --- a/frontend/src/app/util/http.cljs +++ b/frontend/src/app/util/http.cljs @@ -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} diff --git a/frontend/src/app/util/webapi.cljs b/frontend/src/app/util/webapi.cljs index 0833ac70d0..1b3a63b97a 100644 --- a/frontend/src/app/util/webapi.cljs +++ b/frontend/src/app/util/webapi.cljs @@ -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 [] diff --git a/frontend/src/app/util/zip.cljs b/frontend/src/app/util/zip.cljs index ae0d484133..2b8e67ce8a 100644 --- a/frontend/src/app/util/zip.cljs +++ b/frontend/src/app/util/zip.cljs @@ -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]