This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2020-11-15
Channels
- # aleph (5)
- # announcements (1)
- # architecture (35)
- # babashka (9)
- # beginners (87)
- # chlorine-clover (13)
- # cider (3)
- # cljdoc (12)
- # clojure (16)
- # clojure-australia (2)
- # clojure-dev (4)
- # clojure-europe (5)
- # clojuredesign-podcast (21)
- # clojurescript (19)
- # conjure (1)
- # core-async (1)
- # data-science (1)
- # emacs (44)
- # events (2)
- # helix (1)
- # leiningen (2)
- # malli (31)
- # pathom (3)
- # pedestal (31)
- # portal (3)
- # reagent (20)
- # reitit (5)
- # reveal (2)
- # rewrite-clj (18)
- # tools-deps (6)
- # xtdb (5)
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?
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
(defn wrap-with-spa [route]
(fn [req]
(let [res (route req)]
(if (nil? res)
(... single page app ...)
res))))
(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
if one of the interceptors returns a core.async channel, pedestal will handle the juggling of that for you
a whole bunch, in fact most, http frameworks get by with either middleware stacking or some other system
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'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 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)