Stuart Hinson bio photo

Stuart Hinson

Rust & Clojure programmer. Open for freelancing.


LinkedIn Github

Pedestal represents interceptors, its analog to Ring middleware, as data. Let’s look at how we can use that to conditionally inject a ClojureScript REPL into responses.

Pedestal Terminology

Two working definitions as we start

  • Interceptor - a record containing keys corresponding functions for execution stages. Serves an analogous role to middleware in Ring.
  • context - a map with all data relating to processing a request. Contains Ring request and response maps along with additional data.

Marking Routes with Metadata

We’ll start by adding metadata indicating that a browser REPL should be included in the response. We can either add it directly to an endpoint

(require '[io.pedestal.interceptor.helpers :as h]
(require '[ring.util.response :refer [response content-type]])

(def hello-world
     (fn [request]
       (-> (response "<html><body><h1>hello world!!</h1></body></html>")
           (content-type "text/html"))))
    {:browser-repl true}))

or to an interceptor anywhere else in the chain

(def include-browser-repl
     (fn [context] context))
    {:browser-repl true}))

;; in the route definition
["/foo" ^:interceptors [include-browser-repl]
  {:get foo-handler}]

Interceptor Application

Interceptors serve a similar role as middlewares in Ring but the implementation is substantially different. See Pedestal’s docs for a full discussion, but for now the important difference is in how middlewares and interceptors are combined. In Ring, middlewares compose functionally

(defn middleware-ex
  [handler transform-request transform-response]
  (fn [request]
    (let [response (handler (transform-request request))]
      (transform-response response))))

(def new-handler (-> some-handler
                     (middleware-ex fn-1 fn-2)
                     (middleware-ex fn-3 fn-4)))

;; new-handler is now a fn [request] that could be
;; composed with additional middleware

Pedestal represents interceptors sequentially, adding them as a queue to the request / response context (under :io.pedestal.impl.interceptor/queue). In broad strokes, requests are processed by taking an interceptor from the queue and applying its :enter function to the context, producing a new context that the next interceptor in the queue will act on. As each interceptor is visited, it’s added to a stack in the context that will be traversed on the :leave stage. The code is well worth a read.

Knowing that the other interceptors involved in processing a request are visible in the context’s :io.pedestal.impl.interceptor/queue, we can write an interceptor that scans the queue looking for the :browser-repl metadata and injects a repl into the response if it’s found

(require '[io.pedestal.interceptor.helpers :refer [around]])

(defn browser-repl
  "produces an interceptor that scans the context's queue on enter for
  brower-repl metadata, calls inject-repl on leave if found"
   (fn [context] ;; fn to call on :enter
     (cond-> context
       (some->> context ;; scan the queue for :browser-repl metadata
                (map meta)
                (some :browser-repl))
       (assoc :include-browser-repl true))) ;; add a flag to inject the repl
   (fn [{response :response :as context}] ;; on :leave
     (cond-> context
       (:include-browser-repl context) ;; did we find browser-repl metadata?
       (update :response inject-repl))))) ;; add a repl!

We use around to implement browser-repl so that we can examine the interceptor queue in the before stage and set a flag in the context called :include-browser-repl that we’ll read when a response is present in the leave phase and decide whether to call inject-repl.

inject-repl is a function that handles the mechanics of modifying the response, along the lines of

(defn inject-repl
  [{body :body :as response}]
  (let [html (if (string? body)
               (slurp body))
        repl-template (enlive/template
                       (enlive/html-snippet html) []
                        (enlive/html [:script "goog.require(\"\");"])))]

    (->> (repl-template)
         (apply str)

Note Brian Rowe and Alex Redington suggested a couple of very nice extensions to this idea that I’d recommend studying before implementing it.

Kinda cool, right? Interceptors are data, and neat approaches fall out. We use a similar pattern for authorization.

Also, big thanks to Brian Rowe and Rick Hall for a number of improvements to this post!