;; 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 (ns app.main.errors "Generic error handling" (:require [app.common.exceptions :as ex] [app.common.pprint :as pp] [app.common.schema :as sm] [app.main.data.auth :as da] [app.main.data.modal :as modal] [app.main.data.notifications :as ntf] [app.main.data.workspace :as-alias dw] [app.main.router :as rt] [app.main.store :as st] [app.util.globals :as glob] [app.util.i18n :refer [tr]] [app.util.timers :as ts] [cuerdas.core :as str] [potok.v2.core :as ptk])) ;; From app.main.data.workspace we can use directly because it causes a circular dependency (def reload-file nil) (defn- print-data! [data] (-> data (dissoc ::sm/explain) (dissoc :explain) (dissoc ::trace) (dissoc ::instance) (pp/pprint {:width 70}))) (defn- print-explain! [data] (when-let [{:keys [errors] :as explain} (::sm/explain data)] (let [errors (mapv #(update % :schema sm/form) errors)] (pp/pprint errors {:width 100 :level 15 :length 20}))) (when-let [explain (:explain data)] (js/console.log explain))) (defn- print-trace! [data] (some-> data ::trace js/console.log)) (defn- print-group! [message f] (try (js/console.group message) (f) (catch :default _ nil) (finally (js/console.groupEnd message)))) (defn print-cause! [message cause] (print-group! message (fn [] (print-data! cause) (print-explain! cause) (print-trace! cause)))) (defn exception->error-data [cause] (let [data (ex-data cause)] (-> data (assoc :hint (or (:hint data) (ex-message cause))) (assoc ::instance cause) (assoc ::trace (.-stack cause))))) (defn print-error! [cause] (cond (map? cause) (print-cause! (:hint cause "Unexpected Error") cause) (ex/error? cause) (print-cause! (ex-message cause) (ex-data cause)) :else (print-cause! (ex-message cause) (exception->error-data cause)))) (defn on-error "A general purpose error handler." [error] (if (map? error) (ptk/handle-error error) (let [data (exception->error-data error)] (ptk/handle-error data)))) ;; Set the main potok error handler (reset! st/on-error on-error) (defmethod ptk/handle-error :default [error] (st/async-emit! (rt/assign-exception error)) (print-group! "Unhandled Error" (fn [] (print-trace! error) (print-data! error)))) ;; We receive a explicit authentication error; If the uri is for ;; workspace, dashboard, viewer or settings, then assign the exception ;; for show the error page. Otherwise this explicitly clears all ;; profile data and redirect the user to the login page. This is here ;; and not in app.main.errors because of circular dependency. (defmethod ptk/handle-error :authentication [error] (let [message (tr "errors.auth.unable-to-login") uri (rt/get-current-href) show-error? (or (str/includes? uri "workspace") (str/includes? uri "dashboard") (str/includes? uri "view") (str/includes? uri "settings"))] (if show-error? (st/async-emit! (rt/assign-exception error)) (do (st/emit! (da/logout)) (ts/schedule 500 #(st/emit! (ntf/warn message))))))) ;; Error that happens on an active business model validation does not ;; passes an validation (example: profile can't leave a team). From ;; the user perspective a error flash message should be visualized but ;; user can continue operate on the application. Can happen in backend ;; and frontend. (defmethod ptk/handle-error :validation [{:keys [code] :as error}] (print-group! "Validation Error" (fn [] (print-data! error) (print-explain! error))) (cond (= code :invalid-paste-data) (let [message (tr "errors.paste-data-validation")] (st/async-emit! (ntf/show {:content message :type :toast :level :error :timeout 3000}))) (= code :vern-conflict) (st/emit! (ptk/event ::dw/reload-current-file)) (= code :snapshot-is-locked) (let [message (tr "errors.version-locked")] (st/async-emit! (ntf/show {:content message :type :toast :level :error :timeout 3000}))) (= code :only-creator-can-lock) (let [message (tr "errors.only-creator-can-lock")] (st/async-emit! (ntf/show {:content message :type :toast :level :error :timeout 3000}))) (= code :only-creator-can-unlock) (let [message (tr "errors.only-creator-can-unlock")] (st/async-emit! (ntf/show {:content message :type :toast :level :error :timeout 3000}))) (= code :snapshot-already-locked) (let [message (tr "errors.version-already-locked")] (st/async-emit! (ntf/show {:content message :type :toast :level :error :timeout 3000}))) :else (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. (defmethod ptk/handle-error :assertion [error] (ts/schedule #(st/emit! (ntf/show {:content (tr "errors.internal-assertion-error") :type :toast :level :error :timeout 3000}))) (print-group! "Internal Assertion Error" (fn [] (print-trace! error) (print-data! error) (print-explain! error)))) ;; ;; All the errors that happens on worker are handled here. (defmethod ptk/handle-error :worker-error [error] (ts/schedule #(st/emit! (ntf/show {:content (tr "errors.internal-worker-error") :type :toast :level :error :timeout 3000}))) (print-group! "Internal Worker Error" (fn [] (print-data! error)))) ;; Error on parsing an SVG (defmethod ptk/handle-error :svg-parser [_] (ts/schedule #(st/emit! (ntf/show {:content (tr "errors.svg-parser.invalid-svg") :type :toast :level :error :timeout 3000})))) ;; TODO: should be handled in the event and not as general error handler (defmethod ptk/handle-error :comment-error [_] (ts/schedule #(st/emit! (ntf/show {:content (tr "errors.comment-error") :type :toast :level :error :timeout 3000})))) ;; That are special case server-errors that should be treated ;; differently. (derive :not-found ::exceptional-state) (derive :bad-gateway ::exceptional-state) (derive :service-unavailable ::exceptional-state) (defmethod ptk/handle-error ::exceptional-state [error] (when-let [cause (::instance error)] (js/console.log (.-stack cause))) (ts/schedule #(st/emit! (rt/assign-exception error)))) (defn- redirect-to-dashboard [] (let [team-id (:current-team-id @st/state) project-id (:current-project-id @st/state)] (if (and project-id team-id) (st/emit! (rt/nav :dashboard-files {:team-id team-id :project-id project-id})) (set! (.-href glob/location) "")))) (defmethod ptk/handle-error :restriction [{:keys [code] :as error}] (cond (= :migration-in-progress code) (let [message (tr "errors.migration-in-progress" (:feature error)) on-accept (constantly nil)] (st/emit! (modal/show {:type :alert :message message :on-accept on-accept}))) (= :team-feature-mismatch code) (let [message (tr "errors.team-feature-mismatch" (:feature error)) on-accept (constantly nil)] (st/emit! (modal/show {:type :alert :message message :on-accept on-accept}))) (= :file-feature-mismatch code) (let [message (tr "errors.file-feature-mismatch" (:feature error))] (st/emit! (modal/show {:type :alert :message message :on-accept redirect-to-dashboard}))) (= :feature-mismatch code) (let [message (tr "errors.feature-mismatch" (:feature error))] (st/emit! (modal/show {:type :alert :message message :on-accept redirect-to-dashboard}))) (= :feature-not-supported code) (let [message (tr "errors.feature-not-supported" (:feature error))] (st/emit! (modal/show {:type :alert :message message :on-accept redirect-to-dashboard}))) (= :file-version-not-supported code) (let [message (tr "errors.version-not-supported")] (st/emit! (modal/show {:type :alert :message message :on-accept redirect-to-dashboard}))) (= :max-quote-reached code) (let [message (tr "errors.max-quota-reached" (:target error))] (st/emit! (modal/show {:type :alert :message message}))) (or (= :paste-feature-not-enabled code) (= :missing-features-in-paste-content code) (= :paste-feature-not-supported code)) (let [message (tr "errors.feature-not-supported" (:feature error))] (st/emit! (modal/show {:type :alert :message message}))) (= :file-in-components-v1 code) (st/emit! (modal/show {:type :alert :message (tr "errors.deprecated") :link-message {:before (tr "errors.deprecated.contact.before") :text (tr "errors.deprecated.contact.text") :after (tr "errors.deprecated.contact.after") :on-click #(st/emit! (rt/nav :settings-feedback))}})) :else (print-cause! "Restriction Error" error))) ;; This happens when the backed server fails to process the ;; request. This can be caused by an internal assertion or any other ;; uncontrolled error. (defmethod ptk/handle-error :server-error [error] (st/async-emit! (rt/assign-exception error)) (print-group! "Server Error" (fn [] (print-data! (dissoc error :data)) (when-let [werror (:data error)] (cond (= :assertion (:type werror)) (print-group! "Assertion Error" (fn [] (print-data! werror) (print-explain! werror))) :else (print-group! "Unexpected" (fn [] (print-data! werror) (print-explain! werror)))))))) (defonce uncaught-error-handler (letfn [(is-ignorable-exception? [cause] (let [message (ex-message cause)] (or (= message "Possible side-effect in debug-evaluate") (= message "Unexpected end of input") (str/starts-with? message "invalid props on component") (str/starts-with? message "Unexpected token ")))) (on-unhandled-error [event] (.preventDefault ^js event) (when-let [error (unchecked-get event "error")] (when-not (is-ignorable-exception? error) (on-error error))))] (.addEventListener glob/window "error" on-unhandled-error) (fn [] (.removeEventListener glob/window "error" on-unhandled-error))))