From 2c87d90bda088d6d14921a13dfdf023e0a80aa7c Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Fri, 9 Jan 2026 08:37:49 +0200 Subject: [PATCH 1/2] fix: create-exception-middleware for hierarchical keywords Previously, the code was searching among the descendants, not the ancestors, of the error type for an error handler. The test also got this wrong, perhaps due to a mistake in the parameter order of derive. --- .../src/reitit/ring/middleware/exception.clj | 6 +++--- test/clj/reitit/ring/middleware/exception_test.clj | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/reitit-middleware/src/reitit/ring/middleware/exception.clj b/modules/reitit-middleware/src/reitit/ring/middleware/exception.clj index f198c213..2d50a995 100644 --- a/modules/reitit-middleware/src/reitit/ring/middleware/exception.clj +++ b/modules/reitit-middleware/src/reitit/ring/middleware/exception.clj @@ -19,8 +19,8 @@ (recur (.getSuperclass sk) (conj ks sk)) ks))) -(defn- descendants-safe [type] - (when-not (class? type) (descendants type))) +(defn- ancestors-safe [type] + (when-not (class? type) (ancestors type))) (defn- call-error-handler [handlers error request] (let [type (:type (ex-data error)) @@ -29,7 +29,7 @@ (get handlers ex-class) (some (partial get handlers) - (descendants-safe type)) + (ancestors-safe type)) (some (partial get handlers) (super-classes ex-class)) diff --git a/test/clj/reitit/ring/middleware/exception_test.clj b/test/clj/reitit/ring/middleware/exception_test.clj index fc4947e0..6e8e4215 100644 --- a/test/clj/reitit/ring/middleware/exception_test.clj +++ b/test/clj/reitit/ring/middleware/exception_test.clj @@ -11,7 +11,7 @@ (:import (clojure.lang ExceptionInfo) (java.sql SQLException SQLWarning))) -(derive ::kikka ::kukka) +(derive ::kukka ::kikka) (deftest exception-test (letfn [(create From 75faf709e2429a0b8354027ccf3616ce1c4bfbeb Mon Sep 17 00:00:00 2001 From: Joel Kaasinen Date: Fri, 9 Jan 2026 09:26:21 +0200 Subject: [PATCH 2/2] fix: create-exception-middleware for deep hierarchies The code was not finding the closest ancestor to the error type, because `ancestors` is not ordered. Now the code does a DFS to find a nearest ancestor. If the nearest ancestor is non-unique, an arbitrary one is picked. --- .../src/reitit/ring/middleware/exception.clj | 13 +++-- .../reitit/ring/middleware/exception_test.clj | 49 +++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/modules/reitit-middleware/src/reitit/ring/middleware/exception.clj b/modules/reitit-middleware/src/reitit/ring/middleware/exception.clj index 2d50a995..7c8204fd 100644 --- a/modules/reitit-middleware/src/reitit/ring/middleware/exception.clj +++ b/modules/reitit-middleware/src/reitit/ring/middleware/exception.clj @@ -19,17 +19,17 @@ (recur (.getSuperclass sk) (conj ks sk)) ks))) -(defn- ancestors-safe [type] - (when-not (class? type) (ancestors type))) +(defn- find-closest-ancestor [val m] + (or (get m val) + (some #(find-closest-ancestor % m) (parents val)))) (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) - (ancestors-safe type)) + (when-not (class? type) + (find-closest-ancestor type handlers)) (some (partial get handlers) (super-classes ex-class)) @@ -143,6 +143,9 @@ 4) Super Classes of exception 5) The ::default handler + Note! If the closest ancestor for `:type` is not unique, an + arbitrary one is picked. + Example: (require '[reitit.ring.middleware.exception :as exception]) diff --git a/test/clj/reitit/ring/middleware/exception_test.clj b/test/clj/reitit/ring/middleware/exception_test.clj index 6e8e4215..3af666ac 100644 --- a/test/clj/reitit/ring/middleware/exception_test.clj +++ b/test/clj/reitit/ring/middleware/exception_test.clj @@ -147,6 +147,55 @@ (is (= status 500)) (is (= body "too many tries"))))))) +(derive ::table ::object) +(derive ::living ::object) +(derive ::plant ::living) +(derive ::animal ::living) +(derive ::dog ::animal) +(derive ::cat ::animal) +(derive ::garfield ::cat) + +(deftest exception-hierarchy-test + (letfn [(create [f] + (ring/ring-handler + (ring/router + [["/defaults" + {:handler f}]] + {:data {:middleware [(exception/create-exception-middleware + (merge + exception/default-handlers + {::object (constantly (http-response/bad-request "object")) + ::living (constantly (http-response/bad-request "living")) + ::animal (constantly (http-response/bad-request "animal")) + ::cat (constantly (http-response/bad-request "cat"))}))]}}))) + (call [ex-typ] + (let [app (create (fn [_] (throw (ex-info "fail" {:type ex-typ}))))] + (app {:request-method :get, :uri "/defaults"})))] + (let [{:keys [status body]} (call ::object)] + (is (= status 400)) + (is (= body "object"))) + (let [{:keys [status body]} (call ::table)] + (is (= status 400)) + (is (= body "object"))) + (let [{:keys [status body]} (call ::living)] + (is (= status 400)) + (is (= body "living"))) + (let [{:keys [status body]} (call ::plant)] + (is (= status 400)) + (is (= body "living"))) + (let [{:keys [status body]} (call ::animal)] + (is (= status 400)) + (is (= body "animal"))) + (let [{:keys [status body]} (call ::dog)] + (is (= status 400)) + (is (= body "animal"))) + (let [{:keys [status body]} (call ::cat)] + (is (= status 400)) + (is (= body "cat"))) + (let [{:keys [status body]} (call ::garfield)] + (is (= status 400)) + (is (= body "cat"))))) + (deftest spec-coercion-exception-test (let [app (ring/ring-handler (ring/router