diff --git a/modules/reitit-ring/src/reitit/ring/coercion.cljc b/modules/reitit-ring/src/reitit/ring/coercion.cljc index 50f5d4e0..fe21d594 100644 --- a/modules/reitit-ring/src/reitit/ring/coercion.cljc +++ b/modules/reitit-ring/src/reitit/ring/coercion.cljc @@ -81,9 +81,11 @@ (response-coercion-failed! result coercion value request response) result)))))) -;; -;; middleware -;; +(defn encode-error [data] + (-> data + (dissoc :request :response) + (update :coercion protocol/get-name) + (->> (protocol/encode-error (:coercion data))))) (defn- coerce-request [coercers request] (reduce-kv @@ -133,6 +135,20 @@ (let [coercers (request-coercers coercion parameters) coerced (coerce-parameters coercers request)] (handler (impl/fast-assoc request :parameters coerced) respond raise))))))) +(defn handle-coercion-exception [e respond raise] + (let [data (ex-data e)] + (if-let [status (condp = (:type data) + ::request-coercion 400 + ::response-coercion 500 + nil)] + (respond + {:status status + :body (encode-error data)}) + (raise e)))) + +;; +;; middleware +;; (def gen-wrap-coerce-parameters "Middleware for pluggable request coercion. @@ -195,3 +211,24 @@ (coerce-response coercers request (handler request))) ([request respond raise] (handler request #(respond (coerce-response coercers request %)) raise)))))))})) + +(def gen-wrap-coerce-exceptions + "Middleare for coercion exception handling. + Expects a :coercion of type `reitit.coercion.protocol/Coercion` + and :parameters or :responses from route data, otherwise does not mount." + (middleware/create + {:name ::coerce-exceptions + :gen-wrap (fn [{:keys [coercion parameters responses]} _] + (if (and coercion (or parameters responses)) + (fn [handler] + (fn + ([request] + (try + (handler request) + (catch Exception e + (handle-coercion-exception e identity #(throw %))))) + ([request respond raise] + (try + (handler request respond (fn [e] (handle-coercion-exception e respond raise))) + (catch Throwable e + (handle-coercion-exception e respond raise))))))))})) diff --git a/test/cljc/reitit/coercion_test.cljc b/test/cljc/reitit/coercion_test.cljc index ea4dfd19..a9b8a62f 100644 --- a/test/cljc/reitit/coercion_test.cljc +++ b/test/cljc/reitit/coercion_test.cljc @@ -2,62 +2,146 @@ (:require [clojure.test :refer [deftest testing is]] [reitit.ring :as ring] [reitit.ring.coercion :as coercion] - [reitit.ring.coercion.spec :as spec]) + [reitit.ring.coercion.spec :as spec] + [schema.core :as s] + [reitit.ring.coercion.schema :as schema]) #?(:clj (:import (clojure.lang ExceptionInfo)))) -(defn handler - ([{:keys [::mw]}] - {:status 200 :body (conj mw :ok)}) - ([request respond raise] - (respond (handler request)))) +(defn handler [{{{:keys [a]} :query + {:keys [b]} :body + {:keys [c]} :form + {:keys [d]} :header + {:keys [e]} :path} :parameters}] + {:status 200 + :body {:total (+ a b c d e)}}) -(deftest coercion-test - (let [app (ring/ring-handler - (ring/router - ["/api" - ["/plus/:e" - {:get {:parameters {:query {:a int?} - :body {:b int?} - :form {:c int?} - :header {:d int?} - :path {:e int?}} - :responses {200 {:schema {:total pos-int?}}} - :handler (fn [{{{:keys [a]} :query - {:keys [b]} :body - {:keys [c]} :form - {:keys [d]} :header - {:keys [e]} :path} :parameters}] - {:status 200 - :body {:total (+ a b c d e)}})}}]] - {:data {:middleware [coercion/gen-wrap-coerce-parameters - coercion/gen-wrap-coerce-response] - :coercion spec/coercion}}))] +(def valid-request + {:uri "/api/plus/5" + :request-method :get + :query-params {"a" "1"} + :body-params {:b 2} + :form-params {:c 3} + :header-params {:d 4}}) - (testing "all good" - (is (= {:status 200 - :body {:total 15}} - (app {:uri "/api/plus/5" - :request-method :get - :query-params {"a" "1"} - :body-params {:b 2} - :form-params {:c 3} - :header-params {:d 4}})))) +(def invalid-request + {:uri "/api/plus/5" + :request-method :get}) - (testing "invalid request" - (is (thrown-with-msg? - ExceptionInfo - #"Request coercion failed" - (app {:uri "/api/plus/5" - :request-method :get})))) +(def invalid-request2 + {:uri "/api/plus/5" + :request-method :get + :query-params {"a" "1"} + :body-params {:b 2} + :form-params {:c 3} + :header-params {:d -40}}) - (testing "invalid response" - (is (thrown-with-msg? - ExceptionInfo - #"Response coercion failed" - (app {:uri "/api/plus/5" - :request-method :get - :query-params {"a" "1"} - :body-params {:b 2} - :form-params {:c 3} - :header-params {:d -40}})))))) +(deftest spec-coercion-test + (let [create (fn [middleware] + (ring/ring-handler + (ring/router + ["/api" + ["/plus/:e" + {:get {:parameters {:query {:a int?} + :body {:b int?} + :form {:c int?} + :header {:d int?} + :path {:e int?}} + :responses {200 {:schema {:total pos-int?}}} + :handler handler}}]] + {:data {:middleware middleware + :coercion spec/coercion}})))] + + (testing "withut exception handling" + (let [app (create [coercion/gen-wrap-coerce-parameters + coercion/gen-wrap-coerce-response])] + + (testing "all good" + (is (= {:status 200 + :body {:total 15}} + (app valid-request)))) + + (testing "invalid request" + (is (thrown-with-msg? + ExceptionInfo + #"Request coercion failed" + (app invalid-request)))) + + (testing "invalid response" + (is (thrown-with-msg? + ExceptionInfo + #"Response coercion failed" + (app invalid-request2)))))) + + (testing "with exception handling" + (let [app (create [coercion/gen-wrap-coerce-exceptions + coercion/gen-wrap-coerce-parameters + coercion/gen-wrap-coerce-response])] + + (testing "all good" + (is (= {:status 200 + :body {:total 15}} + (app valid-request)))) + + (testing "invalid request" + (let [{:keys [status body]} (app invalid-request)] + (is (= 400 status)))) + + (testing "invalid response" + (let [{:keys [status body]} (app invalid-request2)] + (is (= 500 status)))))))) + +(deftest schema-coercion-test + (let [create (fn [middleware] + (ring/ring-handler + (ring/router + ["/api" + ["/plus/:e" + {:get {:parameters {:query {:a s/Int} + :body {:b s/Int} + :form {:c s/Int} + :header {:d s/Int} + :path {:e s/Int}} + :responses {200 {:schema {:total (s/constrained s/Int pos? 'positive)}}} + :handler handler}}]] + {:data {:middleware middleware + :coercion schema/coercion}})))] + + (testing "withut exception handling" + (let [app (create [coercion/gen-wrap-coerce-parameters + coercion/gen-wrap-coerce-response])] + + (testing "all good" + (is (= {:status 200 + :body {:total 15}} + (app valid-request)))) + + (testing "invalid request" + (is (thrown-with-msg? + ExceptionInfo + #"Request coercion failed" + (app invalid-request)))) + + (testing "invalid response" + (is (thrown-with-msg? + ExceptionInfo + #"Response coercion failed" + (app invalid-request2)))) + + (testing "with exception handling" + (let [app (create [coercion/gen-wrap-coerce-exceptions + coercion/gen-wrap-coerce-parameters + coercion/gen-wrap-coerce-response])] + + (testing "all good" + (is (= {:status 200 + :body {:total 15}} + (app valid-request)))) + + (testing "invalid request" + (let [{:keys [status body]} (app invalid-request)] + (is (= 400 status)))) + + (testing "invalid response" + (let [{:keys [status body]} (app invalid-request2)] + (is (= 500 status))))))))))