clojurering

Why does the order of Ring middleware need to be reversed?


I'm writing some middleware for Ring and I'm really confused as to why I have to reverse the order of the middleware.

I've found this blog post but it doesn't explain why I have to reverse it.

Here's a quick excerpt from the blog post:

(def app
  (wrap-keyword-params (wrap-params my-handler)))

The response would be:

{; Trimmed for brevity
 :params {"my_param" "54"}}

Note that the wrap keyword params didn't get called on it because the params hash didn't exist yet. But when you reverse the order of the middleware like so:

(def app
  (wrap-params (wrap-keyword-params my-handler)))

{; Trimmed for brevity
 :params {:my_param "54"}}

It works.

Could somebody please explain why you have to reverse the order of the middleware?


Solution

  • It helps to visualize what middleware actually is.

    (defn middleware [handler]
      (fn [request]
        ;; ...
        ;; Do something to the request before sending it down the chain.
        ;; ...
        (let [response (handler request)]
          ;; ...
          ;; Do something to the response that's coming back up the chain.
          ;; ...
          response)))
    

    That right there was pretty much the a-ha moment for me.

    What's confusing at first glance is that middleware isn't applied to the request, which is what you're thinking of.

    Recall that a Ring app is just a function that takes a request and returns a response (which means it's a handler):

    ((fn [request] {:status 200, ...}) request)  ;=> response
    

    Let's zoom out a little bit. We get another handler:

    ((GET "/" [] "Hello") request)  ;=> response
    

    Let's zoom out a little more. We find the my-routes handler:

    (my-routes request)  ;=> response
    

    Well, what if you wanted to do something before sending the request to the my-routes handler? You can wrap it with another handler.

    ((fn [req] (println "Request came in!") (my-routes req)) request)  ;=> response
    

    That's a little hard to read, so let's break out for clarity. We can define a function that returns that handler. Middleware are functions that take a handler and wrap it another handler. It doesn't return a response. It returns a handler that can return a response.

    (defn println-middleware [wrapped-func]
      (fn [req]
        (println "Request came in!")
        (wrapped-func req)))
    
    ((println-middleware my-route) request)  ;=> response
    

    And if we need to do something before even println-middleware gets the request, then we can wrap it again:

    ((outer-middleware (println-middleware my-routes)) request)  ;=> response
    

    The key is that my-routes, just like your my-handler, is the only named function that actually takes the request as an argument.

    One final demonstration:

    (handler3 (handler2 (handler1 request)))  ;=> response
    ((middleware1 (middleware2 (middleware3 handler1))) request)  ;=> response
    

    I write so much because I can sympathize. But scroll back up to my first middleware example and hopefully it makes more sense.