[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- :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에 위치시킨 함수들이다.