[220107] Extending Ring Exception Middleware with Reitit
Table of Contents
1 소개
이 내용은 ring의 예외 핸들링을 위한 정보를 담고 있다. 공식사이트(Link)의 정보를 취사선택하여 번역 및 필요하다고 생각되는 정보를 추가로 담았다.
2 reitit/ring/middleware/exception.clj
2.1 exception-middleware
(require '[reitit.ring :as ring]) (def app (ring/ring-handler (ring/router ["/fail" (fn [_] (throw (Exception. "fail")))] {:data {:middleware [exception/exception-middleware]}}))) (app {:request-method :get, :uri "/fail"}) ;{:status 500 ; :body {:type "exception" ; :class "java.lang.Exception"}}
내부구현을 보자.
(def exception-middleware "A preconfigured exception handling Middleware. To configure the exceptions handlers, use `create-exception-handler` instea." {:name ::exception :spec ::spec :wrap (wrap default-handler)})
2.2 wrap
(defn- wrap [handlers] (fn [handler] (fn ([request] (try (handler request) (catch Throwable e (on-exception handlers e request identity #(throw %))))))))
필요에 의해 wrap
함수의 구현을 간단화했다.
wrap
함수가 이 예외핸들링의 핵심이다. 이 함수는 middleware를 리턴한다. 이 미들웨어는 handler를 실행할 때 try문으로 전부 감싸기 시작한다. 예외가 던져지면 catch문에 던져지면서 on-exception
함수가 수행된다.
2.3 on-exception function
(defn- on-exception [handlers e request response raise] (try (response (call-error-handler handlers e request)) (catch Exception e (raise e))))
response
=identity
raise
=#(throw %)
치환하면 아래처럼 된다.
(defn- on-exception [handlers e request response raise] (try (identity (call-error-handler handlers e request)) (catch Exception e (throw e))))
call-error-handler
를 호출하고 리턴한다. 만약에 이것마저 실패하면 그냥 던져버린다.
2.4 call-error-handler
(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-class ex-class)) (get handlers ::default))] (if-let [wrap (get handlers ::wrap)] (wrap error-handler error request) (error-handler error request))))
일단 error
에서 핸들러를 찾아내야 한다.
여기에는 여러가지 규칙이 있다. 눈에 띄는 것은 :type
, ::default
, ::wrap
예외를 어떻게 핸들링 하는지 관련 키에 들어있나보다. 이런건 어디에 설명되어 있을까?
2.5 create-exception-middleware
공식문서에 디폴트로 만들어준 예외 핸들러가 있다.
:reitit.ring/response
: value in ex-data key:response
will be returned.:muuntaja/decode
: handle Muuntaja decoding exceptions.:reitit.coercion/request-coercion
: request coercion errors (http 400 response):reitit.coercion/response-coercion
: response coercion errors (http 500 response)::exception/default
: a default exception handler if nothing else matched (default exception/default-handler).::exception/wrap
: a 3-arity handler to wrap the actual handler handler exception request => response (no default).
핸들러는 예외를 다루기 위한 여러옵션이 있는 맵에서 다음 순서로 예외 식별자(exception identifier)에 의해 선택된다.
:type
of exception ex-data- Class of exception
:type
ancestors of exception ex-data- Super class of exception
- The
::default
handler
이 순서는 call-error-handler
의 구현를 나타내기도 한다.
특이한 점이 예외 클리스의 상속인 값들도 사용한다는 것이다. 또한 ex-data
예외에 리턴하는 :type
값에 hierarchy를 지정하고 그것도 핸들링 해준다는 것이다. 즉 우리는 ex-data
에 :type
을 지정해서 좀 더 세부적인 예외를 던지고 핸들링 할 수 있다. 즉, 새로운 클래스를 만드는 것보다. ex-data
에 :type
을 추가하는 것을 더 선호하는 듯 하다.
이제 우리는 이런 예외 핸들링을 위한 자료가 해시맵으로 되어 있다는 것을 알았다.
즉, 만약에 ::exception/default
를 다르게 바꾸고 싶다면, default-handler
의 ::exception/handler
키에 다른 함수를 넣기만 하면 된다.
;; type hierarchy (derive ::error ::exception) (derive ::failure ::exception) (derive ::horror ::exception) (defn handler [message exception request] {:status 500 :body {:message message :exception (.getClass exception) :data (ex-data exception) :uri (:uri request)}}) (def exception-middleware (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] (println "ERROR" (pr-str (:uri request))) (handler e request))}))) (def app (ring/ring-handler (ring/router ["/fail" (fn [_] (throw (ex-info "fail" {:type ::failure})))] {:data {:middleware [exception-middleware]}}))) (app {:request-method :get, :uri "/fail"}) ; ERROR "/fail" ; => {:status 500, ; :body {:message "default" ; :exception clojure.lang.ExceptionInfo ; :data {:type :user/failure} ; :uri "/fail"}}
이제 이해될 것이다. call-error-handler
는 우리가 핸들링을 위해 해시맵의 key에 위치시킨 함수들이다.