Created
June 24, 2021 22:32
-
-
Save escherize/140e44bf829b1c062b8b097820d313d3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; 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