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)에 의해 선택된다.

  1. :type of exception ex-data
  2. Class of exception
  3. :type ancestors of exception ex-data
  4. Super class of exception
  5. 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에 위치시킨 함수들이다.

Date: 2022-01-07 Fri 00:00

Author: Younghwan Nam

Created: 2022-09-14 Wed 01:26

Emacs 27.2 (Org mode 9.4.4)

Validate