pedestal

hequ 2020-11-15T17:58:25.132900Z

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?

emccue 2020-11-15T18:02:30.133400Z

@hequ Let me answer that with a practical use

emccue 2020-11-15T18:04:04.134300Z

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

emccue 2020-11-15T18:04:07.134600Z

;; ----------------------------------------------------------------------------
(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))})

emccue 2020-11-15T18:06:37.136100Z

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

emccue 2020-11-15T18:07:08.136900Z

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

emccue 2020-11-15T18:07:15.137100Z

the other way would look like

emccue 2020-11-15T18:08:22.138300Z

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

emccue 2020-11-15T18:09:08.138700Z

A better example might be this

emccue 2020-11-15T18:09:18.139Z

(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)))})

emccue 2020-11-15T18:09:58.139600Z

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

emccue 2020-11-15T18:11:18.140900Z

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

emccue 2020-11-15T18:11:44.141400Z

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

emccue 2020-11-15T18:12:09.142Z

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

emccue 2020-11-15T18:12:29.142400Z

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

emccue 2020-11-15T18:12:45.142800Z

I guess it really isn't a hard need to

emccue 2020-11-15T18:13:23.143600Z

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

emccue 2020-11-15T18:14:21.144200Z

but there are some concrete benefits to doing it this way

emccue 2020-11-15T18:15:02.144700Z

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

emccue 2020-11-15T18:16:04.145300Z

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

emccue 2020-11-15T18:16:29.145900Z

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

hequ 2020-11-15T18:31:27.150600Z

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 Kottmann 2020-11-15T18:33:38.152400Z

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 Kottmann 2020-11-15T18:33:42.152600Z

it's pretty nifty

Louis Kottmann 2020-11-15T18:34:57.153900Z

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)

Louis Kottmann 2020-11-15T18:39:07.154100Z

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

Louis Kottmann 2020-11-15T18:40:05.154300Z

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 Kottmann 2020-11-15T18:41:10.154500Z

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 Kottmann 2020-11-15T18:41:28.154700Z

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

hequ 2020-11-15T18:43:18.155Z

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

hequ 2020-11-15T18:55:38.156100Z

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! 🙂