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?
@hequ Let me answer that with a practical use
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
;; ----------------------------------------------------------------------------
(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))})
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
since you have the :response
as just a detail of how the different layers communicate
the other way would look like
(defn wrap-with-spa [route]
(fn [req]
(let [res (route req)]
(if (nil? res)
(... single page app ...)
res))))
A better example might be this
(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)))})
Since the interceptors are a chain, we can do meta stuff like re-arrange or add steps to what comes next
The other thing it allows is seperation of "async" steps
if one of the interceptors returns a core.async channel, pedestal will handle the juggling of that for you
and no other interceptor needs to know that what was returned happened asynchronously
(afaik, i haven't tested that for anything)
I guess it really isn't a hard need to
a whole bunch, in fact most, http frameworks get by with either middleware stacking or some other system
but there are some concrete benefits to doing it this way
since the same "chain of steps that happen one after another" model can be generalized to more than http
(huge grains of salt - I have been frequently befuddled by pedestal and that hasn't stopped)
(but I have made decent progress writing my hobby app with it so i at least kinda sorta know some things)
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?
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
it's pretty nifty
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)
I'd say use :enter
interceptors to build your response, and use :leave
interceptors to massage it
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
you could do it in an :enter
interceptor but as soon as an interceptor returns a context
with a :response
it will start leaving
so to make a reusable default it is in its own :leave
interceptor
oh, so you can control the flow by attaching a response to the context. Well that’s nice.
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! 🙂