Fork me on GitHub
#ring
<
2021-03-22
>
zendevil.eth18:03:59

I’m sending a request with the following params {:user/foo 1 :user/bar 2}, however, in the request map in the server, this map is converted to: {:foo 1 :bar 2} . Is there a way to preserve the namespacing?

seancorfield18:03:55

How exactly are you sending the request to your Ring server?

seancorfield18:03:21

And what middleware do you have active in your Ring server?

seancorfield18:03:01

Are you sending that hash map as text, JSON, EDN, Transit?

zendevil.eth19:03:16

@seancorfield I’m using this library to send the request: https://github.com/day8/re-frame-http-fx This is my :http-xhrio map:

{:method :get
   :uri "/my/uri"
   :params {:user/foo 1 :user/bar 2}
   :on-success [:on-success]
   :on-failure [:on-failure]
   :response-format (edn/edn-response-format)
   :format (edn/edn-request-format)
   }  
The middleware active in my ring server are:
{:middleware [#(wrap-keyword-params % {:parse-namespaces? true})
                 middleware/wrap-formats
                 wrap-multipart-params
                 ]}                

seancorfield20:03:11

Can you check in your browser dev tools that the URL being sent to the server actually has the qualified query parameter names? Since you’re using a GET you should see /my/uri?user/foo=1&user/bar=2 being sent. Or possibly with : in front of user in each slot?

seancorfield20:03:26

(I would expect POST params to be sent as qualified names I think, although you may have to ask in #re-frame to be sure of that)

seancorfield20:03:48

Looking at wrap-keyword-params with :parse-namespaces? true it looks like it should preserve the qualifier in any string params that arrive at the server. But I’d probably add some debugging at the outermost layer of your middleware to print the whole Ring request to be certain that you are getting the raw string data at the server as you expect. You don’t have wrap-params in your middleware stack so the :query-params and :form-params may not be processed into the common :params slot that wrap-keyword-params expects (middleware is hard — which is why I always use ring-defaults to try to get everything all in one place).

seancorfield20:03:37

OK, good that confirms the browser is sending what you expect. Now add some debugging on the server side to see what is in the raw Ring request at the outermost layer.

zendevil.eth20:03:45

@seancorfield does ring-defaults come with qualified keywords?

zendevil.eth20:03:38

What is the outermost layer? Since I’m using the luminus framework, ring is a complete black-box to me

seancorfield20:03:01

This is why I do not recommend Luminus to beginners 😞

zendevil.eth20:03:10

I’m not even sure if #(wrap-keyword-params % {:parse-namespaces? true}) is the correct way to specify that middleware

zendevil.eth20:03:09

would finding out about the outermost layer entail a print statement somewhere in the app-routes function in handler.clj?:

(mount/defstate app-routes
  :start
  (ring/ring-handler
    (ring/router
      [(home-routes)])
    (ring/routes
      (ring/create-resource-handler
        {:path "/"})
      (wrap-content-type
        (wrap-webjars (constantly nil)))
      (ring/create-default-handler
        {:not-found
         (constantly (error-page {:status 404, :title "404 - Page not found"}))
         :method-not-allowed
         (constantly (error-page {:status 405, :title "405 - Not allowed"}))
         :not-acceptable
         (constantly (error-page {:status 406, :title "406 - Not acceptable"}))}))))

seancorfield20:03:02

Oh, Luminus uses Mount as well 😞 Something else I advise beginners to avoid.

seancorfield20:03:15

Okay, so your debugging would need to be in the form of a middleware function that you would write that would need to go in that :middleware vector. I’m not sure which end because I don’t know what order Luminus wraps the middleware, so I’d probably add it to both ends.

seancorfield20:03:30

(defn debug [h] (fn [req] (println req) (h req)))
is middleware that prints the whole Ring request (and still invokes the handler on the request).

seancorfield20:03:19

So you’d have:

{:middleware [debug
                 #(wrap-keyword-params % {:parse-namespaces? true})
                 middleware/wrap-formats
                 wrap-multipart-params
                 debug
                 ]}   

seancorfield20:03:44

That should let you see what’s coming into the server at one end and what’s going into your handler at the other end.

zendevil.eth20:03:44

I’m not seeing the print statements when the request comes in

seancorfield20:03:54

Did you restart the server with that new middleware in place?

zendevil.eth20:03:02

Upon restarting the server, I do see the logs. And the params don’t have qualified keywords on both ends

seancorfield20:03:56

OK, I just confirmed in the REPL that you need wrap-params as well as wrap-keyword-params:

dev=> ((p/wrap-params (kp/wrap-keyword-params identity {:parse-namespaces? true})) {:query-string "user/foo=1&user/bar=2"})
{:query-string "user/foo=1&user/bar=2", :form-params {}, :params #:user{:foo "1", :bar "2"}, :query-params {"user/foo" "1", "user/bar" "2"}}
dev=> ((kp/wrap-keyword-params identity {:parse-namespaces? true}) {:query-string "user/foo=1&user/bar=2"})
{:query-string "user/foo=1&user/bar=2", :params nil}

seancorfield20:03:50

p is an alias for ring.middleware.wrap-params, kp is ring.middleware.wrap-keyword-params

seancorfield20:03:56

Here’s my complete REPL session where I debugged this so you can see how I did it:

dev=> (require '[ring.middleware.keyword-params :as kp])
nil
dev=> ((kp/wrap-keyword-params identity {:parse-namespaces? true}) {:query-params {"user/foo" "1" "user/bar" "2"}})
{:query-params {"user/foo" "1", "user/bar" "2"}, :params nil}
dev=> (require '[ring.middleware.params :as p])
nil
dev=> ((kp/wrap-keyword-params (p/wrap-params identity) {:parse-namespaces? true}) {:query-params {"user/foo" "1" "user/bar" "2"}})
{:query-params {"user/foo" "1", "user/bar" "2"}, :params {}, :form-params {}}
dev=> ((p/wrap-params (kp/wrap-keyword-params identity {:parse-namespaces? true})) {:query-params {"user/foo" "1" "user/bar" "2"}})
{:query-params {"user/foo" "1", "user/bar" "2"}, :form-params {}, :params {}}
dev=> ((p/wrap-params (kp/wrap-keyword-params identity {:parse-namespaces? true})) {:query-string "user/foo=1&user/bar=2"})
{:query-string "user/foo=1&user/bar=2", :form-params {}, :params #:user{:foo "1", :bar "2"}, :query-params {"user/foo" "1", "user/bar" "2"}}
dev=> ((kp/wrap-keyword-params identity {:parse-namespaces? true}) {:query-string "user/foo=1&user/bar=2"})
{:query-string "user/foo=1&user/bar=2", :params nil}
I initially thought it would depend on :query-params but then I realized that is created by wrap-params from :query-string.

seancorfield20:03:46

But that’s how you can debug the pieces via the REPL. identity is just taking the place of a handler function that does nothing (just returns the input Ring request) so it’s easier to debug.

seancorfield20:03:58

And just to show what happens if you have them the other way round in the middleware stack:

dev=> ((kp/wrap-keyword-params (p/wrap-params identity) {:parse-namespaces? true}) {:query-string "user/foo=1&user/bar=2"})
{:query-string "user/foo=1&user/bar=2", :params {"user/foo" "1", "user/bar" "2"}, :form-params {}, :query-params {"user/foo" "1", "user/bar" "2"}}

seancorfield20:03:28

Having middleware in the wrong order is often the same as not having it at all:

dev=> ((p/wrap-params identity) {:query-string "user/foo=1&user/bar=2"})
{:query-string "user/foo=1&user/bar=2", :form-params {}, :params {"user/foo" "1", "user/bar" "2"}, :query-params {"user/foo" "1", "user/bar" "2"}}

seancorfield20:03:43

As for ring-defaults, yes, you can have it parse namespaces but that isn’t the default — and it’s not entirely obvious how to tell it to do it. You need to pass something like (assoc-in api-defaults [:params :keywordize] {:parse-namespaces? true}) I think, instead of just api-defaults (you may want to tweak other settings too — it’s all just data).

seancorfield20:03:02

The nice thing about ring-defaults is it provides out-of-the-box stacks of middleware in the correct order for both API style apps and website style apps (the latter has CSRF enabled by default and a few other differences), and you can also control how HTTP vs HTTPS behaves and several other useful things very easily.

seancorfield20:03:20

But all this is why I recommend beginners avoid “frameworks” like Luminus and start with just Ring at first and learn how Ring works on its own, and then either learn Compojure or Reitit for routing (mapping URLs to handlers), and then learn Component or Integrant for managing the start/stop lifecycle of components (web servers, database connection pools, anything that has some process to “start” it up and another process to “stop” it when you’re done).

zendevil.eth20:03:21

@seancorfield I tried:

{:middleware [debug
                 wrap-params
                 #(wrap-keyword-params % {:parse-namespaces? true})
                 middleware/wrap-formats
                 wrap-multipart-params
                 debug
                 ]}
and also:
{:middleware [debug
                 #(wrap-keyword-params % {:parse-namespaces? true})
                 wrap-params
                 middleware/wrap-formats
                 wrap-multipart-params
                 debug
                 ]}
But I’m not seeing the qualified keywords in the params. Could it possibly be because of other middleware?

seancorfield21:03:12

Hard to be sure — as I said, I’ve no idea how Luminus rolls that middleware vector up.

seancorfield21:03:43

That suggests you probably want wrap-params at the end (after wrap-multipart-params).

zendevil.eth21:03:08

@seancorfield, in my request, I checked {…:data {:middleware …}} and found this:

{:middleware [#function[humboiserver.routes.home/debug] #function[ring.middleware.params/wrap-params] #function[humboiserver.routes.home/home-routes/fn--19313] #function[humboiserver.middleware/wrap-formats] #function[ring.middleware.multipart-params/wrap-multipart-params] #function[humboiserver.routes.home/debug]]

zendevil.eth21:03:23

It seems like wrap-keyword-params isn’t being applied

zendevil.eth21:03:35

No, actually: #function[humboiserver.routes.home/home-routes/fn--19313] is that

seancorfield21:03:57

#(..) is an anonymous fn so, yes, #function[humboiserver.routes.home/home-routes/fn--19313] is that 🙂

seancorfield21:03:38

As I said above, looking at ring-defaults, I think you want:

{:middleware [debug
                 #(wrap-keyword-params % {:parse-namespaces? true})
                 middleware/wrap-formats
                 wrap-multipart-params
                 wrap-params
                 debug
                 ]}

seancorfield21:03:14

BTW, the wrap-formats middleware there is specific to Luminus and is a conditional wrapper around this library: https://github.com/metosin/muuntaja

seancorfield21:03:34

(just so you understand how many “moving parts” are being pulled in by Luminus)

zendevil.eth21:03:25

@seancorfield I tried this order but still no qualified keywords

seancorfield21:03:45

And you restarted your server again?

seancorfield21:03:21

Then maybe try in #luminus — if this was plain ol’ Ring, what I showed above would solve this.

seancorfield21:03:42

But, seriously, consider abandoning Luminus for now and learn the basics so you can debug this sort of stuff yourself.

zendevil.eth21:03:30

@seancorfield the incoming :query-string doesn’t have qualified namespaces in the log. I mean, it’s just foo=1&bar=2 and not user/foo=1&user/bar=2

seancorfield21:03:56

Since you’re using Luminus, I have no idea at this point.

seancorfield21:03:30

My advice is to build a very simple pure Ring server and make sure you can get it working with that.

zendevil.eth21:03:46

@seancorfield In pure ring, is it true that the query-string always contains the qualified keywords whether or not “wrap-keyword-params parse-namespace true” and “wrap params” middleware are applied?

zendevil.eth21:03:11

@seancorfield, I confirmed that it doesn’t have to do with #luminus . I do get qualified keywords when I make the request from the browser with “user/foo=1…“, but it doesn’t work with the reframe http library

seancorfield22:03:55

When I asked about what browser devtools showed, at the beginning of this discussion, I meant while you were using the re-frame app — that was the first thing you needed to verify 😞

seancorfield22:03:08

I hope that the REPL session showing how to debug middleware was useful?

zendevil.eth22:03:43

@seancorfield yes it was useful thanks 🙏