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