Fork me on GitHub
#pedestal
<
2020-11-15
>
hequ17:11:25

What’s the point of interceptors having both enter and leave functions? Why are interceptors not “just functions” which can be executed in order? I read the interceptors reference but I’m having a hard time understanding what is the benefit of having both enter and leave functions? And why the request has to “make a loop” by first executing all enter functions and then all the leave functions?

emccue18:11:30

@hequ Let me answer that with a practical use

emccue18:11:04

First, an interceptor I wrote that sees if a later interceptor has filled in a :response, and if its a get request will fill in the standard Single Page App code

emccue18:11:07

;; ----------------------------------------------------------------------------
(def serve-elm-on-404
  "Intercepts any unhandled get requests and serves the SPA."
  {:leave
   (fn [context]
     (if (and (not (http/response? (:response context)))
              (= :get (:request-method (:request context))))
       (assoc
         context
         :response
         {:status 200
          :headers {"Content-Type" "text/html"}
          :body
          (page/html5
            {:lang "en"}
            [:html
             [:head
              [:meta {:charset "UTF-8"}]
              [:style "body { padding: 0; margin: 0; }"]]
             [:body
              [:div {:id "mount"}]
              [:script {:type "text/javascript"} (-> (io/resource "public/app.js") ;; TODO: Cache
                                                     (slurp))]
              [:script {:type "text/javascript"}
               "var app = Elm.Main.init({node: document.getElementById('mount')});"]]])})
       context))})

emccue18:11:37

You can write this in the "function stacking" way for sure, so its not the best example, but it is a case where it is pretty clear what is going on

emccue18:11:08

since you have the :response as just a detail of how the different layers communicate

emccue18:11:15

the other way would look like

emccue18:11:22

(defn wrap-with-spa [route]
   (fn [req]
      (let [res (route req)]
        (if (nil? res)
           (... single page app ...)
           res))))

emccue18:11:08

A better example might be this

emccue18:11:18

(def requires-auth-interceptor
  "An interceptor that looks for a user in the request map and
  (if there is none), early returns a 401. "
  {:name ::requires-auth-interceptor
   :enter
   (fn [context]
     (let [{:keys [user]} (:request context)]
       (if-not user
         (-> context
             (assoc :response unauthorized-response)
             (interceptor-chain/terminate))
         context)))})

emccue18:11:58

Since the interceptors are a chain, we can do meta stuff like re-arrange or add steps to what comes next

emccue18:11:18

The other thing it allows is seperation of "async" steps

emccue18:11:44

if one of the interceptors returns a core.async channel, pedestal will handle the juggling of that for you

emccue18:11:09

and no other interceptor needs to know that what was returned happened asynchronously

emccue18:11:29

(afaik, i haven't tested that for anything)

emccue18:11:45

I guess it really isn't a hard need to

emccue18:11:23

a whole bunch, in fact most, http frameworks get by with either middleware stacking or some other system

emccue18:11:21

but there are some concrete benefits to doing it this way

emccue18:11:02

since the same "chain of steps that happen one after another" model can be generalized to more than http

emccue18:11:04

(huge grains of salt - I have been frequently befuddled by pedestal and that hasn't stopped)

emccue18:11:29

(but I have made decent progress writing my hobby app with it so i at least kinda sorta know some things)

hequ18:11:27

Thank you for the concrete examples! So to test my understanding: if your route is about getting some user-data from a database table you could write an enter interceptor which could (for example) first check if that data is already available in some cache and early exit if it is? And if not then there is an enter interceptor which could fetch that user-data and then one or more leave interceptors which would transform that data to a correct response format? Or does it even matter if those are enter or leave interceptors?

Louis Kottmann18:11:07

I'd say use :enter interceptors to build your response, and use :leave interceptors to massage it

Louis Kottmann18:11:05

for example there is a http/json-body that comes with pedestal that will set the content-type to application/json if no content-type is already set

Louis Kottmann18:11:10

you could do it in an :enter interceptor but as soon as an interceptor returns a context with a :response it will start leaving

Louis Kottmann18:11:28

so to make a reusable default it is in its own :leave interceptor

hequ18:11:18

oh, so you can control the flow by attaching a response to the context. Well that’s nice.

Louis Kottmann18:11:38

I have complex a chain of boolean logic which would be tens of levels of nested conditionals with short-circuits via cond now it is a pedestal interceptor (non-http) chain and they are all packaged in their nice little interceptors, which makes it composable for other chains as well

Louis Kottmann18:11:42

it's pretty nifty

Louis Kottmann18:11:57

and yeah there are gotchas, for example I got bitten earlier today because I was declare-ing interceptors before providing them and then defining them, and pedestal crashed because some declared interceptors were "unbound" and it could not check their type so I had to reorder the code in the correct order (and lose some readability)

hequ18:11:38

I probably have too limited understanding for now as my use cases are a pretty basic rest api. But thanks anyway for providing some light on this topic! 🙂