diff --git a/doc/ring/extra_middleware.md b/doc/ring/extra_middleware.md index 82b61a8d..16b7afe9 100644 --- a/doc/ring/extra_middleware.md +++ b/doc/ring/extra_middleware.md @@ -1,6 +1,6 @@ # Default Middleware -Any Ring middleware can be used with `reitit-ring`, using data-driven middleware is preferred as the configuration , and in many cases, better performance. +Any Ring middleware can be used with `reitit-ring`, but using data-driven middleware is preferred as they are easier to manage and in many cases, yield better performance. To make web development easier, `reitit-middleware` contains a set of common ring middleware, lifted into data-driven middleware. @@ -8,58 +8,81 @@ To make web development easier, `reitit-middleware` contains a set of common rin [metosin/reitit-middleware "0.2.0-SNAPSHOT"] ``` -Any Ring middlware can be used with `reitit-ring`. +## Exception handling -`ring-handler` injects the `Match` into a request and it can be extracted at runtime with `reitit.ring/get-match`. This can be used to build ad-hoc extensions to the system. - -Example middleware to guard routes based on user roles: +A polished version of [compojure-api](https://github.com/metosin/compojure-api) exception handling. Catches all exceptions and invokes configured exception handler. ```clj -(require '[reitit.ring :as ring]) -(require '[clojure.set :as set]) - -(defn wrap-enforce-roles [handler] - (fn [{:keys [::roles] :as request}] - (let [required (some-> request (ring/get-match) :data ::roles)] - (if (and (seq required) (not (set/subset? required roles))) - {:status 403, :body "forbidden"} - (handler request))))) +(require '[reitit.ring.middleware.exception :as exception]) ``` -Mounted to an app via router data (effecting all routes): +### `exception/exception-middleware` + +A preconfigured Middleware using `exception/default-handlers`. Catches: + +* request & response [Coercion](coercion.md) exceptions +* [Muuntaja](https://github.com/metosin/muuntaja) decode exceptions +* Exceptions with `:type` of `:reitit.ring/response`, returning `:response` key from `ex-data`. +* safely all other exceptions + +### `exception/create-exception-middleware` + +Creates a reitit middleware 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 +|--------------|------------- +| `::default` | a default exception handler if nothing else mathced (default `exception/default-handler`). +| `::wrap` | a 3-arity handler to wrap the actual handler `handler exception request => response` (no default). + +The handler is selected from the options map by exception idenfitifier 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 ```clj -(def handler (constantly {:status 200, :body "ok"})) +(require '[reitit.ring.middleware.exception :as exception]) -(def app - (ring/ring-handler - (ring/router - [["/api" - ["/ping" handler] - ["/admin" {::roles #{:admin}} - ["/ping" handler]]]] - {:data {:middleware [wrap-enforce-roles]}}))) +;; 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/create-exception-middleware + (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))})) ``` -Anonymous access to public route: +## Content Negotiation -```clj -(app {:request-method :get, :uri "/api/ping"}) -; {:status 200, :body "ok"} -``` +Wrapper for [Muuntaja](https://github.com/metosin/muuntaja) middleware for content-negotiation, request decoding and response encoding. Reads configuration from route data and emit's [swagger](swagger.md) `:produces` and `:consumes` definitions automatically. -Anonymous access to guarded route: +## Multipart request handling -```clj -(app {:request-method :get, :uri "/api/admin/ping"}) -; {:status 403, :body "forbidden"} -``` - -Authorized access to guarded route: - -```clj -(app {:request-method :get, :uri "/api/admin/ping", ::roles #{:admin}}) -; {:status 200, :body "ok"} -``` - -Dynamic extensions are nice, but we can do much better. See [data-driven middleware](data_driven_middleware.md) and [compiling routes](compiling_middleware.md). +Wrapper for [Ring Multipart Middleware](https://github.com/ring-clojure/ring/blob/master/ring-core/src/ring/middleware/multipart_params.clj). Conditionally mounts to an endpoint only if it has `:multipart` params defined. Emits swagger `:consumes` definitions automatically. diff --git a/modules/reitit-middleware/src/reitit/ring/middleware/exception.clj b/modules/reitit-middleware/src/reitit/ring/middleware/exception.clj index ef4fb1b7..1db6e7e4 100644 --- a/modules/reitit-middleware/src/reitit/ring/middleware/exception.clj +++ b/modules/reitit-middleware/src/reitit/ring/middleware/exception.clj @@ -1,7 +1,10 @@ (ns reitit.ring.middleware.exception (:require [reitit.coercion :as coercion] [reitit.ring :as ring] - [clojure.spec.alpha :as s])) + [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])) @@ -28,7 +31,9 @@ (partial get handlers) (super-classes ex-class)) (get handlers ::default))] - (error-handler error request))) + (if-let [wrap (get handlers ::wrap)] + (wrap error-handler error request) + (error-handler error request)))) (defn- on-exception [handlers e request respond raise] (try @@ -36,7 +41,7 @@ (catch Exception e (raise e)))) -(defn- wrap [{:keys [handlers]}] +(defn- wrap [handlers] (fn [handler] (fn ([request] @@ -50,6 +55,9 @@ (catch Throwable e (on-exception handlers e request respond raise))))))) +(defn print! [^PrintWriter writer & more] + (.write writer (str (str/join " " more) "\n"))) + ;; ;; handlers ;; @@ -78,84 +86,92 @@ :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-options - {: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)}}) +(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 wrap-exception - "Ring middleware that catches all exceptions and looks up a - exceptions handler of type `exception request => response` to - handle the exception. - - The following options are supported: - - | key | description - |--------------|------------- - | `:handlers` | A map of exception identifier => exception-handler - - The handler is selected from the handlers by exception idenfiter - in the following lookup order: - - 1) `:type` of exception ex-data - 2) Class of exception - 3) descadents `:type` of exception ex-data - 4) Super Classes of exception - 5) The ::default handler" - [handler options] - (-> options wrap handler)) + ([handler] + (handler default-handlers)) + ([handler options] + (-> options wrap handler))) (def exception-middleware - "Reitit middleware that catches all exceptions and looks up a - exceptions handler of type `exception request => response` to - handle the exception. - - The following options are supported: - - | key | description - |--------------|------------- - | `:handlers` | A map of exception identifier => exception-handler - - The handler is selected from the handlers by exception idenfiter - in the following lookup order: - - 1) `:type` of exception ex-data - 2) Class of exception - 3) descadents `:type` of exception ex-data - 4) Super Classes of exception - 5) The ::default handler" {:name ::exception :spec ::spec - :wrap (wrap default-options)}) + :wrap (wrap default-handlers)}) (defn create-exception-middleware - "Creates a reitit middleware that catches all exceptions and looks up a - exceptions handler of type `exception request => response` to - handle the exception. + "Creates a reitit middleware 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 options are supported: + The following handlers special handlers are available: | key | description |--------------|------------- - | `:handlers` | A map of exception identifier => exception-handler + | `::default` | a default exception handler if nothing else mathced (default [[default-handler]]). + | `::wrap` | a 3-arity handler to wrap the actual handler `handler exception request => response` - The handler is selected from the handlers by exception idenfiter + 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) descadents `:type` of exception ex-data + 3) `:type` ancestors of exception ex-data 4) Super Classes of exception - 5) The ::default handler" + 5) The ::default handler + + Example: + + (require '[reitit.ring.middleware.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/create-exception-middleware + (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))}))" ([] - (create-exception-middleware default-options)) - ([options] + (create-exception-middleware default-handlers)) + ([handlers] {:name ::exception :spec ::spec - :wrap (wrap options)})) + :wrap (wrap handlers)})) diff --git a/test/clj/reitit/ring/middleware/exception_test.clj b/test/clj/reitit/ring/middleware/exception_test.clj index 9f897c78..ad8c01d7 100644 --- a/test/clj/reitit/ring/middleware/exception_test.clj +++ b/test/clj/reitit/ring/middleware/exception_test.clj @@ -10,23 +10,27 @@ (derive ::kikka ::kukka) (deftest exception-test - (letfn [(create [f] - (ring/ring-handler - (ring/router - [["/defaults" - {:handler f}] - ["/coercion" - {:middleware [reitit.ring.coercion/coerce-request-middleware - reitit.ring.coercion/coerce-response-middleware] - :coercion reitit.coercion.spec/coercion - :parameters {:query {:x int?, :y int?}} - :responses {200 {:body {:total pos-int?}}} - :handler f}]] - {:data {:middleware [(exception/create-exception-middleware - (update - exception/default-options :handlers merge - {::kikka (constantly {:status 200, :body "kikka"}) - SQLException (constantly {:status 200, :body "sql"})}))]}})))] + (letfn [(create + ([f] + (create f nil)) + ([f wrap] + (ring/ring-handler + (ring/router + [["/defaults" + {:handler f}] + ["/coercion" + {:middleware [reitit.ring.coercion/coerce-request-middleware + reitit.ring.coercion/coerce-response-middleware] + :coercion reitit.coercion.spec/coercion + :parameters {:query {:x int?, :y int?}} + :responses {200 {:body {:total pos-int?}}} + :handler f}]] + {:data {:middleware [(exception/create-exception-middleware + (merge + exception/default-handlers + {::kikka (constantly {:status 400, :body "kikka"}) + SQLException (constantly {:status 400, :body "sql"}) + ::exception/wrap wrap}))]}}))))] (testing "normal calls work ok" (let [response {:status 200, :body "ok"} @@ -81,20 +85,32 @@ (testing "exact :type" (let [app (create (fn [_] (throw (ex-info "fail" {:type ::kikka}))))] - (is (= {:status 200, :body "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 200, :body "kikka"} + (is (= {:status 400, :body "kikka"} (app {:request-method :get, :uri "/defaults"}))))) (testing "exact Exception" (let [app (create (fn [_] (throw (SQLException.))))] - (is (= {:status 200, :body "sql"} + (is (= {:status 400, :body "sql"} (app {:request-method :get, :uri "/defaults"}))))) (testing "Exception SuperClass" (let [app (create (fn [_] (throw (SQLWarning.))))] - (is (= {:status 200, :body "sql"} + (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"})))))))