Skip to content

Instantly share code, notes, and snippets.

@escherize
Created June 24, 2021 22:32
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save escherize/140e44bf829b1c062b8b097820d313d3 to your computer and use it in GitHub Desktop.
Save escherize/140e44bf829b1c062b8b097820d313d3 to your computer and use it in GitHub Desktop.
;; Middleware is a way to define a stack of operations applied to a function
;; call. It's a common pattern for parsing and handling http requests. A
;; handler function is wrapped by a middleware function. Let's narrow in on
;; http handling, then:
;; Let's call a 'handler function' a function that accepts as its input a
;; request, and returns a response.
(defn my-handler-fn [request]
{:status 200 :body "42"})
;; Next, A 'middleware function' takes as its argument a handler function, and
;; returns a handler function. Ignoring side effects, this means middleware
;; functions can effect the request/response on the way in, or on the way out.
;; e.g. neither:
(defn my-noop-middleware [handler]
(fn [request]
(handler request)))
;; e.g. on the way in: (changing the request)
(defn my-add-start-timing-middleware [handler]
(fn [request]
(handler
(assoc request :start-time (new java.util.Date))))) ;; <-- assoc into request
;; e.g. on the way out: (changing the response)
(defn my-add-end-timing-middleware [handler]
(fn [request]
(let [response (handler request)]
(assoc response :end-time (new java.util.Date))))) ;; <- assoc into response
;; e.g. both:
(defn my-add-all-timing-middleware [handler]
(fn [request]
(let [response (handler (assoc request :start-time (new java.util.Date))) ;; <-- assoc into request
end-time (new java.util.Date)]
(assoc response :total-time "difference of start and end")))) ;; <- assoc into response
;; Let's apply one of our middlewares to my-handler-fn and use it:
(def wrapped-handler-fn (my-add-end-timing-middleware my-handler-fn))
(wrapped-handler-fn {})
;;=> {:status 200, :body "42", :end-time #inst "2021-06-24T22:15:43.024-00:00"}
;; Often these sorts of middlware functions are stacked together and do a number
;; of rudimentary operations. An example might look like:
(comment
(-> handler
(wrap-user-logged-in?)
(wrap-with-request-id)
(wrap-contet-type)
(wrap-4xx-errors)
(wrap-ceo-accessing-analytics-numbers)
(wrap-logging)
(wrap-timestamping)))
;; When you wrap multiple middlewares using a threading macro (as is the best
;; practice) it's important to understand exactly how the wrapping works.
;; As you can imagine it can become difficult to know which middleware
;; function acts on which piece of the request/response. In the above example,
;; as a request comes into our middleware-wrapped handler function, the first
;; thing that processes that request is 'wrap-timestamping'. However it is
;; also the last middleware function to potentially act upon the response we
;; finally return to our client.
;;
;; So, let's print out the actual middleware stack that a request/response travels through.
;; First these act on the REQUEST in this order:
;;
;; wrap-timestamping
;; wrap-logging
;; wrap-ceo-accessing-analytics-numbers
;; wrap-4xx-errors
;; wrap-contet-type
;; wrap-with-request-id
;; wrap-user-logged-in?
;; Then the handler is called:
;;
;; handler
;; Then the RESPONSE is acted on in the opposite order like so:
;;
;; wrap-user-logged-in?
;; wrap-with-request-id
;; wrap-contet-type
;; wrap-4xx-errors
;; wrap-ceo-accessing-analytics-numbers
;; wrap-logging
;; wrap-timestamping
;; Therefore when saying that a certain middleware comes "before" another one,
;; it's important to note wether you mean "on the way in" (aka acting on the
;; response) or "on the way out" (acting on the request).
;; There are many middlewares that only act on either the request or the
;; response, but usually they're just tossed into the middleware stack and it
;; can be hard to reason about what middleware does what job.
;; So I've created the on-the-way-in and on-the-way-out middleware helper functions.
;; Using these we can simplify many of the middleware functions while adding
;; information to the middleware stack about what each one can do.
;; e.g.
'(-> handler
(on-the-way-in user-logged-in?)
(wrap-with-request-id)
(on-the-way-out contet-type?)
(on-the-way-out wrap-4xx-errors)
(wrap-ceo-accessing-analytics-numbers)
(on-the-way-in request-logging)
(on-the-way-in log-timestamping))
;; Now for some live examples:
(let [handler identity ;; <-- our 'handler' is just the identity function
;; example of building a middlware stack with on-the-way-{in,out}:
;; notice that these are stacked in alphabetical order: ----------------+
mw-stack (-> handler ;; v
(on-the-way-out #(log/spy (update % :passed-through conj :out/a))) ;; <- wraps handler most closely
(on-the-way-in #(log/spy (update % :passed-through conj :in/b)))
(on-the-way-in #(log/spy (update % :passed-through conj :in/c)))
(on-the-way-out #(log/spy (update % :passed-through conj :out/d))))] ;; <- the first and last mw in mw-stack.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Request / Response Lifecycle
;;
;; a on-the-way-out ------------------------------------------------+
;; b on-the-way-in -------+ |
;; c on-the-way-in | |
;; | | |
;; v v v
;; d( --req-> c( --req-> b( --req-> a( --req-> [handler] --resp-> /a) --resp-> /b) --resp-> /c) --resp-> /d) --resp->
;; ^
;; |
;; d on-the-way-out ---------------------------------------------------------------------------------------+
;; When following the request on its journey to become a response, notice
;; how the request is passed from left to right, through:
;; c in, b in, a out, d out
;; in that order.
;; Exercise: what is returned from this call?
(mw-stack {:passed-through []}))
;; Now for a more real-world challenge. compojure/wrap-routes only applies
;; middleware to routes that match. so let's try adapting our middleware
;; helpers to it:
(defroutes my-routes
(GET "/" x (select-keys x [:passed-through])))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; how it looks without mw:
(my-routes
{:uri "/"
:request-method :get
:passed-through ["hi"]})
;;=>
{:status 200, :headers {}, :body "", :passed-through ["hi"]}
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; With Middlewares:
(let [mw-stack (-> my-routes
(compojure/wrap-routes (fn [h] (on-the-way-out h #(update % :passed-through conj "out"))))
(compojure/wrap-routes (fn [h] (on-the-way-in h #(update % :passed-through conj "in")))))]
(mw-stack
{:uri "/"
:request-method :get
:passed-through []}))
;;=>
{:status 200, :headers {}, :body "", :passed-through ["in" "out"]}
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; With single arg Middlewares:
(let [mw-stack (-> my-routes
(compojure/wrap-routes (on-the-way-out #(update % :passed-through conj "out")))
(compojure/wrap-routes (on-the-way-in #(update % :passed-through conj "in"))))]
(mw-stack
{:uri "/"
:request-method :get
:passed-through []}))
;;=>
{:status 200, :headers {}, :body "", :passed-through ["in" "out"]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment