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 ( with-meta ( h/handler ( 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 ( with-meta ( h/before ( 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" [ inject-repl ] ( around ::browser-repl ( fn [ context ] ;; fn to call on :enter ( cond-> context ( some->> context ;; scan the queue for :browser-repl metadata :io.pedestal.impl.interceptor/queue ( 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 ) body ( slurp body )) repl-template ( enlive/template ( enlive/html-snippet html ) [] [ :body ] ( enlive/append ( enlive/html [ :script "goog.require(\"pedestal_browser_repl.dev\");" ])))] ( ->> ( repl-template ) ( apply str ) ( response ))))

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!