mirror of
https://github.com/metosin/reitit.git
synced 2025-12-17 00:11:11 +00:00
exceptions
This commit is contained in:
parent
de3fc480b4
commit
ca02680e2d
2 changed files with 271 additions and 0 deletions
|
|
@ -0,0 +1,152 @@
|
|||
(ns reitit.http.interceptors.exception
|
||||
(:require [reitit.coercion :as coercion]
|
||||
[reitit.ring :as ring]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.string :as str])
|
||||
(:import (java.time Instant)
|
||||
(java.io PrintWriter)))
|
||||
|
||||
(s/def ::handlers (s/map-of any? fn?))
|
||||
(s/def ::spec (s/keys :opt-un [::handlers]))
|
||||
|
||||
;;
|
||||
;; helpers
|
||||
;;
|
||||
|
||||
(defn- super-classes [^Class k]
|
||||
(loop [sk (.getSuperclass k), ks []]
|
||||
(if-not (= sk Object)
|
||||
(recur (.getSuperclass sk) (conj ks sk))
|
||||
ks)))
|
||||
|
||||
(defn- call-error-handler [handlers error request]
|
||||
(let [type (:type (ex-data error))
|
||||
ex-class (class error)
|
||||
error-handler (or (get handlers type)
|
||||
(get handlers ex-class)
|
||||
(some
|
||||
(partial get handlers)
|
||||
(descendants type))
|
||||
(some
|
||||
(partial get handlers)
|
||||
(super-classes ex-class))
|
||||
(get handlers ::default))]
|
||||
(if-let [wrap (get handlers ::wrap)]
|
||||
(wrap error-handler error request)
|
||||
(error-handler error request))))
|
||||
|
||||
(defn print! [^PrintWriter writer & more]
|
||||
(.write writer (str (str/join " " more) "\n")))
|
||||
|
||||
;;
|
||||
;; handlers
|
||||
;;
|
||||
|
||||
(defn default-handler
|
||||
"Default safe handler for any exception."
|
||||
[^Exception e _]
|
||||
{:status 500
|
||||
:body {:type "exception"
|
||||
:class (.getName (.getClass e))}})
|
||||
|
||||
(defn create-coercion-handler
|
||||
"Creates a coercion exception handler."
|
||||
[status]
|
||||
(fn [e _]
|
||||
{:status status
|
||||
:body (coercion/encode-error (ex-data e))}))
|
||||
|
||||
(defn http-response-handler
|
||||
"Reads response from Exception ex-data :response"
|
||||
[e _]
|
||||
(-> e ex-data :response))
|
||||
|
||||
(defn request-parsing-handler [e _]
|
||||
{:status 400
|
||||
:headers {"Content-Type" "text/plain"}
|
||||
:body (str "Malformed " (-> e ex-data :format pr-str) " request.")})
|
||||
|
||||
(defn wrap-log-to-console [handler e {:keys [uri request-method] :as req}]
|
||||
(print! *out* (Instant/now) request-method (pr-str uri) "=>" (.getMessage e))
|
||||
(.printStackTrace e *out*)
|
||||
(handler e req))
|
||||
|
||||
;;
|
||||
;; public api
|
||||
;;
|
||||
|
||||
(def default-handlers
|
||||
{::default default-handler
|
||||
::ring/response http-response-handler
|
||||
:muuntaja/decode request-parsing-handler
|
||||
::coercion/request-coercion (create-coercion-handler 400)
|
||||
::coercion/response-coercion (create-coercion-handler 500)})
|
||||
|
||||
(defn exception-interceptor
|
||||
"Creates an Interceptor that catches all exceptions. Takes a map
|
||||
of `identifier => exception request => response` that is used to select
|
||||
the exception handler for the thown/raised exception identifier. Exception
|
||||
idenfier is either a `Keyword` or a Exception Class.
|
||||
|
||||
The following handlers special handlers are available:
|
||||
|
||||
| key | description
|
||||
|------------------------|-------------
|
||||
| `::exception/default` | a default exception handler if nothing else mathced (default [[default-handler]]).
|
||||
| `::exception/wrap` | a 3-arity handler to wrap the actual handler `handler exception request => response`
|
||||
|
||||
The handler is selected from the options map by exception idenfiter
|
||||
in the following lookup order:
|
||||
|
||||
1) `:type` of exception ex-data
|
||||
2) Class of exception
|
||||
3) `:type` ancestors of exception ex-data
|
||||
4) Super Classes of exception
|
||||
5) The ::default handler
|
||||
|
||||
Example:
|
||||
|
||||
(require '[reitit.ring.interceptors.exception :as exception])
|
||||
|
||||
;; type hierarchy
|
||||
(derive ::error ::exception)
|
||||
(derive ::failure ::exception)
|
||||
(derive ::horror ::exception)
|
||||
|
||||
(defn handler [message exception request]
|
||||
{:status 500
|
||||
:body {:message message
|
||||
:exception (str exception)
|
||||
:uri (:uri request)}})
|
||||
|
||||
(exception/exception-interceptor
|
||||
(merge
|
||||
exception/default-handlers
|
||||
{;; ex-data with :type ::error
|
||||
::error (partial handler \"error\")
|
||||
|
||||
;; ex-data with ::exception or ::failure
|
||||
::exception (partial handler \"exception\")
|
||||
|
||||
;; SQLException and all it's child classes
|
||||
java.sql.SQLException (partial handler \"sql-exception\")
|
||||
|
||||
;; override the default handler
|
||||
::exception/default (partial handler \"default\")
|
||||
|
||||
;; print stack-traces for all exceptions
|
||||
::exception/wrap (fn [handler e request]
|
||||
(.printStackTrace e)
|
||||
(handler e request))}))"
|
||||
([]
|
||||
(exception-interceptor default-handlers))
|
||||
([handlers]
|
||||
{:name ::exception
|
||||
:spec ::spec
|
||||
:error (fn [ctx]
|
||||
(let [error (:error ctx)
|
||||
request (:request ctx)
|
||||
response (call-error-handler handlers error request)]
|
||||
(if (instance? Exception response)
|
||||
(-> ctx (assoc :error response) (dissoc :response))
|
||||
(-> ctx (assoc :response response) (dissoc :error)))))}))
|
||||
119
test/clj/reitit/http/interceptors/exception_test.clj
Normal file
119
test/clj/reitit/http/interceptors/exception_test.clj
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
(ns reitit.http.interceptors.exception-test
|
||||
(:require [clojure.test :refer [deftest testing is]]
|
||||
[reitit.ring :as ring]
|
||||
[reitit.http :as http]
|
||||
[reitit.http.interceptors.exception :as exception]
|
||||
[reitit.interceptor.sieppari :as sieppari]
|
||||
[reitit.coercion.spec]
|
||||
[reitit.http.coercion]
|
||||
[muuntaja.core :as m])
|
||||
(:import (java.sql SQLException SQLWarning)))
|
||||
|
||||
(derive ::kikka ::kukka)
|
||||
|
||||
(deftest exception-test
|
||||
(letfn [(create
|
||||
([f]
|
||||
(create f nil))
|
||||
([f wrap]
|
||||
(http/ring-handler
|
||||
(http/router
|
||||
[["/defaults"
|
||||
{:handler f}]
|
||||
["/coercion"
|
||||
{:interceptors [(reitit.http.coercion/coerce-request-interceptor)
|
||||
(reitit.http.coercion/coerce-response-interceptor)]
|
||||
:coercion reitit.coercion.spec/coercion
|
||||
:parameters {:query {:x int?, :y int?}}
|
||||
:responses {200 {:body {:total pos-int?}}}
|
||||
:handler f}]]
|
||||
{:data {:interceptors [(exception/exception-interceptor
|
||||
(merge
|
||||
exception/default-handlers
|
||||
{::kikka (constantly {:status 400, :body "kikka"})
|
||||
SQLException (constantly {:status 400, :body "sql"})
|
||||
::exception/wrap wrap}))]}})
|
||||
{:executor sieppari/executor})))]
|
||||
|
||||
(testing "normal calls work ok"
|
||||
(let [response {:status 200, :body "ok"}
|
||||
app (create (fn [_] response))]
|
||||
(is (= response (app {:request-method :get, :uri "/defaults"})))))
|
||||
|
||||
(testing "unknown exception"
|
||||
(let [app (create (fn [_] (throw (NullPointerException.))))]
|
||||
(is (= {:status 500
|
||||
:body {:type "exception"
|
||||
:class "java.lang.NullPointerException"}}
|
||||
(app {:request-method :get, :uri "/defaults"}))))
|
||||
(let [app (create (fn [_] (throw (ex-info "fail" {:type ::invalid}))))]
|
||||
(is (= {:status 500
|
||||
:body {:type "exception"
|
||||
:class "clojure.lang.ExceptionInfo"}}
|
||||
(app {:request-method :get, :uri "/defaults"})))))
|
||||
|
||||
(testing "::ring/response"
|
||||
(let [response {:status 200, :body "ok"}
|
||||
app (create (fn [_] (throw (ex-info "fail" {:type ::ring/response, :response response}))))]
|
||||
(is (= response (app {:request-method :get, :uri "/defaults"})))))
|
||||
|
||||
(testing ":muuntaja/decode"
|
||||
(let [app (create (fn [_] (m/decode m/instance "application/json" "{:so \"invalid\"}")))]
|
||||
(is (= {:body "Malformed \"application/json\" request."
|
||||
:headers {"Content-Type" "text/plain"}
|
||||
:status 400}
|
||||
(app {:request-method :get, :uri "/defaults"}))))
|
||||
|
||||
(testing "::coercion/request-coercion"
|
||||
(let [app (create (fn [{{{:keys [x y]} :query} :parameters}]
|
||||
{:status 200, :body {:total (+ x y)}}))]
|
||||
|
||||
(let [{:keys [status body]} (app {:request-method :get
|
||||
:uri "/coercion"
|
||||
:query-params {"x" "1", "y" "2"}})]
|
||||
(is (= 200 status))
|
||||
(is (= {:total 3} body)))
|
||||
|
||||
(let [{:keys [status body]} (app {:request-method :get
|
||||
:uri "/coercion"
|
||||
:query-params {"x" "abba", "y" "2"}})]
|
||||
(is (= 400 status))
|
||||
(is (= :reitit.coercion/request-coercion (:type body))))
|
||||
|
||||
(let [{:keys [status body]} (app {:request-method :get
|
||||
:uri "/coercion"
|
||||
:query-params {"x" "-10", "y" "2"}})]
|
||||
(is (= 500 status))
|
||||
(is (= :reitit.coercion/response-coercion (:type body)))))))
|
||||
|
||||
(testing "exact :type"
|
||||
(let [app (create (fn [_] (throw (ex-info "fail" {:type ::kikka}))))]
|
||||
(is (= {:status 400, :body "kikka"}
|
||||
(app {:request-method :get, :uri "/defaults"})))))
|
||||
|
||||
(testing "parent :type"
|
||||
(let [app (create (fn [_] (throw (ex-info "fail" {:type ::kukka}))))]
|
||||
(is (= {:status 400, :body "kikka"}
|
||||
(app {:request-method :get, :uri "/defaults"})))))
|
||||
|
||||
(testing "exact Exception"
|
||||
(let [app (create (fn [_] (throw (SQLException.))))]
|
||||
(is (= {:status 400, :body "sql"}
|
||||
(app {:request-method :get, :uri "/defaults"})))))
|
||||
|
||||
(testing "Exception SuperClass"
|
||||
(let [app (create (fn [_] (throw (SQLWarning.))))]
|
||||
(is (= {:status 400, :body "sql"}
|
||||
(app {:request-method :get, :uri "/defaults"})))))
|
||||
|
||||
(testing "::exception/wrap"
|
||||
(let [calls (atom 0)
|
||||
app (create (fn [_] (throw (SQLWarning.)))
|
||||
(fn [handler exception request]
|
||||
(if (< (swap! calls inc) 2)
|
||||
(handler exception request)
|
||||
{:status 500, :body "too many tries"})))]
|
||||
(is (= {:status 400, :body "sql"}
|
||||
(app {:request-method :get, :uri "/defaults"})))
|
||||
(is (= {:status 500, :body "too many tries"}
|
||||
(app {:request-method :get, :uri "/defaults"})))))))
|
||||
Loading…
Reference in a new issue