[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=identityraise=#(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:responsewill 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)에 의해 선택된다.
:typeof exception ex-data- Class of exception
:typeancestors of exception ex-data- Super class of exception
- The
::defaulthandler
이 순서는 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에 위치시킨 함수들이다.