Middleware for catching exceptions thrown by the query processor and returning them in a friendlier format. | (ns metabase.query-processor.middleware.catch-exceptions (:require [metabase.lib.schema.common :as lib.schema.common] [metabase.query-processor.error-type :as qp.error-type] [metabase.query-processor.middleware.permissions :as qp.perms] [metabase.query-processor.pipeline :as qp.pipeline] [metabase.query-processor.schema :as qp.schema] [metabase.util :as u] [metabase.util.i18n :refer [trs]] [metabase.util.log :as log] [metabase.util.malli :as mu]) (:import (clojure.lang ExceptionInfo) (java.sql SQLException))) |
(set! *warn-on-reflection* true) | |
Format an Exception thrown by the Query Processor into a userland error response map. | (defmulti ^:private format-exception
{:arglists '([^Throwable e])}
class) |
(defmethod format-exception Throwable
[^Throwable e]
{:status :failed
:class (class e)
:error (.getMessage e)
:stacktrace (u/filtered-stacktrace e)}) | |
(defmethod format-exception InterruptedException
[^InterruptedException _e]
{:status :interrupted}) | |
(defmethod format-exception ExceptionInfo
[e]
;; `:is-curated` is a flag that signals whether the error message in `e` was approved by product
;; to be shown to the user. It is used by FE.
(let [{error-type :type, is-curated :is-curated, :as data} (ex-data e)]
(merge
((get-method format-exception Throwable) e)
(when (qp.error-type/known-error-type? error-type)
{:error_type error-type})
(when is-curated
{:error_is_curated is-curated})
;; TODO - we should probably change this key to `:data` so we're not mixing lisp-case and snake_case keys
{:ex-data data}))) | |
(defmethod format-exception SQLException
[^SQLException e]
(assoc ((get-method format-exception Throwable) e)
:state (.getSQLState e))) | |
Exception chain in reverse order, e.g. inner-most cause first. TODO -- some of this logic duplicates the functionality of | (defn- exception-chain [e] (reverse (u/full-exception-chain e))) |
In cases where the top-level Exception doesn't have the best error message, return a better one to use instead. We usually want to show SQLExceptions at the top level since they contain more useful information. | (mu/defn- best-top-level-error
[maps :- [:sequential {:min 1} :map]]
(some (fn [m]
(when (isa? (:class m) SQLException)
(select-keys m [:error])))
maps)) |
(mu/defn exception-response :- [:map [:status :keyword]]
"Convert an Exception to a nicely-formatted Clojure map suitable for returning in userland QP responses."
[^Throwable e :- (lib.schema.common/instance-of-class Throwable)]
(let [[m & more :as maps] (for [e (exception-chain e)]
(format-exception e))]
(merge
m
(best-top-level-error maps)
;; merge in the first error_type we see
(when-let [error-type (some :error_type maps)]
{:error_type error-type})
(when (seq more)
{:via (vec more)})))) | |
Map of about | (defn- query-info
[{query-type :type, :as query} {:keys [preprocessed native]}]
(merge
{:json_query (dissoc query :info :driver)}
;; add the fully-preprocessed and native forms to the error message for MBQL queries, since they're extremely
;; useful for debugging purposes.
(when (= (keyword query-type) :query)
{:preprocessed preprocessed
:native (when (qp.perms/current-user-has-adhoc-native-query-perms? query)
native)}))) |
(mu/defn- query-execution-info :- :map [query-execution :- :map] (dissoc query-execution :result_rows :hash :executor_id :dashboard_id :pulse_id :native :start_time_millis)) | |
(mu/defn- format-exception* :- [:map [:status :keyword]]
"Format a `Throwable` into the usual userland error-response format."
[query :- :map
^Throwable e :- (lib.schema.common/instance-of-class Throwable)
extra-info :- [:maybe :map]]
(try
;; [[metabase.query-processor.middleware.process-userland-query/process-userland-query-middleware]] wraps exceptions
;; to add query execution info, unwrap them and format the wrapped one
(if-let [query-execution (:query-execution (ex-data e))]
(merge (query-execution-info query-execution)
(format-exception* query (ex-cause e) extra-info))
(merge
{:data {:rows [], :cols []}, :row_count 0}
(exception-response e)
(query-info query extra-info)))
(catch Throwable e
(assoc (Throwable->map e) :status :failed)))) | |
(mu/defn catch-exceptions :- ::qp.schema/qp
"Middleware for catching exceptions thrown by the query processor and returning them in a 'normal' format. Forwards
exceptions to the `result-chan`."
[qp :- ::qp.schema/qp]
(mu/fn [query :- ::qp.schema/query
rff :- ::qp.schema/rff]
(if-not (get-in query [:middleware :userland-query?])
(qp query rff)
(let [extra-info (delay
{:native (u/ignore-exceptions
((requiring-resolve 'metabase.query-processor.compile/compile) query))
:preprocessed (u/ignore-exceptions
((requiring-resolve 'metabase.query-processor.preprocess/preprocess) query))})]
(try
(qp query rff)
(catch Throwable e
;; format the Exception and return it
(let [formatted-exception (format-exception* query e @extra-info)]
(log/errorf "Error processing query: %s\n%s"
(or (:error formatted-exception) "Error running query")
(u/pprint-to-str formatted-exception))
;; ensure always a message on the error otherwise FE thinks query was successful. (#23258, #23281)
(let [result (update formatted-exception
:error (fnil identity (trs "Error running query")))]
(assert (:status result))
(qp.pipeline/*result* result))))))))) | |