Fork me on GitHub
Rikard Mustajärvi14:02:39

Hi! I've been debugging an issue where a middleware is being applied twice. I've narrowed it down to a smallish test case which looks like this:

(deftest middleware-being-applied-twice
  (testing "middleware shouldn't be applied twice"
    (let [merge-routers (fn [& routers]
                           (apply merge (map r/routes routers))
                           (apply merge (map r/options routers))))
          counter (atom 0)
          middleware (fn [handler]
                       (fn [request]
                         (swap! counter inc)
                         (prn "counter:" @counter)
                         (handler request)))

          handler (fn [_req] {:status 200 :body @counter})

          router1 (ring/router
                   [["/one" {:roles #{:public} :get handler}]])
          router2 (ring/router
                   [["/two" {:roles #{:public} :get handler}]]
                   {:data {:middleware [middleware]}})
          merged-routers (merge-routers router1 router2)

          app (ring/routes (ring/ring-handler merged-routers))

          result (app {:request-method :get, :uri "/two"})]
      (is (= 1 @counter))
      (prn result)
      (prn @counter))))
It seems to happen when the two routers are merged. Any ideas about how to make it only apply once?


Merge route trees instead of routers. Routers are separate functions, if you middlewares have side-effects, both router functions will execute them.


Or... ring-handler is a function at least,hmph.


Aha and merge takes routes from both routers and then creates one router with route trees from both


If you just want to separate some routes per namespace etc, you don't need to create router for both, you could just define parts of route tree data. And you don't need to use router to add route-data to every route under route tree branch:

(def tree1 [["/one" ...]])
(def tree2 [["/two" {:middleware [middleware]} ...]])
(def router (ring/router [tree1 tree2])
Doesn't really solve this, but goes around at least.

Rikard Mustajärvi14:02:15

Yes, admittedly a better solution, but the problem is that in the real app there are actually two routers which needs to be merged into a single one.


I'm not sure, but I think when you create router, in your code, the :data option is "compiled" into every route inside router. Then on the merge the routes from r/routes call already include the middleware, and now the options merge adds another middleware when the new router is created to each route.


Try (apply merge (map #(dissoc (r/options %) :data) routers)) to drop :data option from the merge, as they are already applied to routes (if I guess correctly what is happening.)

👍 1
Rikard Mustajärvi14:02:35

Thanks. That makes the test pass. But that will also not apply the middleware to the "/one" route, right..?

Rikard Mustajärvi14:02:01

I'll see if I can rearrange some things and apply the router wide middleware after the merging has taken place.


Router creation does "resolve-routes" and that will apply route data merge, including route data from router opts:


Other router options probably don't affect the r/routes result the same way

Rikard Mustajärvi14:02:06

Ok, thanks. Good to know. Will fiddle with it some more.

Rikard Mustajärvi14:02:34

Hmm, can I create a new router from an existing router while also adding some route data?


yeah, but for the meta merge will concat e.g. vector values

Rikard Mustajärvi14:02:59

Ok, I think I have something which looks like it could work. Thanks for your help.


if route already has {:middleware [1]} and router options has {:data {:middleware [2]}} the resulting route in the router has :middleware [1 2]

Rikard Mustajärvi14:02:08

Right. I've been down that path of debugging a little. I saw no option for doing some type of distinct meta merge so I ended up with duplicates in my vectors.

Rikard Mustajärvi14:02:41

Doing something along the lines of

merged-routers (merge-routers
                           {:data {:middleware [middleware]}}))
Seemed to do the trick. Thanks again for your help.


How do I coerce a list query parameter? /products?productIds=1,2 I would expect productIds = [1 2]

[["/products" {:get {:parameters {:query [:map [:productIds [:sequential int?]]]}
                           :responses  {200 {:body [:sequential int?]}}
                           :handler    (fn [{{productIds :query} :parameters}]
                                         {:status 200
                                          :body   (:productIds productIds)})}}]]


[:sequential {:decode/string (fn [s] (str/split s #","))} :int]

👍 2