Interceptor

Pedestal calls the :enter function on the way “in” to handling a request. It calls the :leave function on the way back “out”. This is shown here for a single interceptor:

All the :enter functions are called in order. Each one receives the context map and returns a (possibly modified) context map. Once all the interceptors have been called, the resulting context map gets passed through the interceptors’ :leave functions, but in reverse order, as shown here:

Example

(require '[exoscale.interceptor :as interceptor])

(def interceptor-A {:name  :A
                    :enter (fn [ctx] (update ctx :a inc))
                    :leave (fn [ctx] (assoc ctx :foo :bar))
                    :error (fn [ctx err] ctx)})

(def interceptor-B {:name  :B
                    :enter (fn [ctx] (update ctx :b inc))
                    :error (fn [ctx err] ctx)})

(def interceptor-C {:name  :C
                    :enter (fn [ctx] (update ctx :c inc))})

;; enter & leave
(interceptor/execute {:a 0 :b 0 :c 0}
                     [interceptor-A
                      interceptor-B
                      interceptor-C])
;=>
{:a   1
 :b   1
 :c   1
 :foo :bar
 :exoscale.interceptor/queue <-()-<
 :exoscale.interceptor/stack ()}

The Queue and the Stack

Pedestal keeps a queue of interceptors that still need to have their :enter functions called. There is also a stack of interceptors whose :enter functions have been called, but their :leave functions have not. As such, the :leave functions are called in reverse order to the :enter functions.

Suppose we start with three interceptors in the queue, like this:

Pedestal needs to call the :enter function on “Intc 1”. So it pops that interceptor from the queue and moves it to the stack. Then it calls the interceptor, passing the context map itself. This is the context as it appears to “Intc 1”:

When that’s done, the next thing is to call “Intc 2”. Same thing happens, Pedestal pops that interceptor from the queue and pushes it on the stack:

Repeat the process for “Intc 3” and we’re left with this context map:

Manipulating the Queue

Both the queue and the stack reside in the context map. Since interceptors can modify the context map, that means they can change the plan of execution for the rest of the request. Interceptors are allowed to enqueue more interceptors to be called, or they can terminate the request.

An example where we dynamically decide which interceptor to add, depending on a query parameter:

(def odds {:name  ::odds
           :enter (fn [ctx] (assoc ctx :msg "I handle odd number"))})

(def evens {:name  ::evens
            :enter (fn [ctx] (assoc ctx :msg "Even numbers are my bag"))})

(def chooser
  {:name  ::chooser
   :enter (fn [ctx]
              (let [n (:n ctx)
                    nxt (if (even? n) evens odds)]
                   (interceptor/enqueue ctx [nxt])))})


(interceptor/execute {:n 0} [chooser])
;=>
{:n 0,
 :msg "Even numbers are my bag"}

(interceptor/execute {:n 1} [chooser])
;=>
{:n 1,
 :msg "I handle odd number"}

Interceptors or Middleware

Middleware models wrap up the whole processing chain in function closures. Not only are they opaque, but the decisions are all made at compile time.

In contrast, Pedestal treats the processing chain like a virtual call stack, or a malleable program to execute.

Error Handling

The :error function is a bit special.

If an interceptor throws an exception, then Pedestal starts looking for an interceptor with an :error function to handle it. This goes from right-to-left like the :leave functions.

The main difference is that an error-handling interceptor may indicate that the error is totally resolved and Pedestal will resume looking for :leave functions.

(def interceptor-A {:name  :A
                    :enter (fn [ctx] (update ctx :a inc))
                    :leave (fn [ctx] (assoc ctx :foo :bar))
                    :error (fn [ctx err] ctx)})

(def interceptor-B {:name  :B
                    :enter (fn [ctx] (update ctx :b #(Integer/parseInt %)))
                    :error (fn [ctx err]
                               (if (instance? java.lang.NumberFormatException err)
                                 (assoc ctx :msg ":b isn't a number!")
                                 (assoc ctx ::interceptor/error err)))})

(def interceptor-C {:name  :C
                    :enter (fn [ctx] (update ctx :c inc))})


; in case we got NumberFormatException on interceptor-B
; interceptor-C is not executed
; :error on intercepter-B is executed & return a context map.
; So next :leave on interceptor-A is call.
(interceptor/execute {:a 0 :b "x" :c 0}
                     [interceptor-A
                      interceptor-B
                      interceptor-C])
;=>
{:a 1,
 :b "x",
 :c 0,
 :msg ":b isn't a number!",
 :foo :bar}


; in case we got ClassCastException on interceptor-B
; interceptor-C is not executed
; :error on intercepter-B is executed & add ::error to context map.
; So next :error on interceptor-A is call instead of :leave
(interceptor/execute {:a 0 :b 0 :c 0}
                     [interceptor-A
                      interceptor-B
                      interceptor-C])
;=>
{:a 1,
 :b 0,
 :c 0}

Async

Ref

  • http://pedestal.io/reference/interceptors
  • http://pedestal.io/guides/what-is-an-interceptor
  • https://github.com/exoscale/interceptor