0

I have a clojure ring server which I want to post data to so I can do stuff in my database such as add, update and delete users. I'm currently stuck on receiving the body in a format I can conveniently use ( a key map).

I come from a NodeJS background where you can easily access the request body by adding the body-parser middleware.

This is the code

(ns questhero.core
  (:require
   [ring.adapter.jetty :as jetty]
   [cheshire.core :refer [generate-string]]
   [ring.middleware.cors :refer [wrap-cors]]
   [ring.util.request]
   [ring.util.io]
   [ring.middleware.defaults :refer [wrap-defaults site-defaults]]))

(defn create-user [request]
  (let [body (slurp (:body request))]
    (println "body" body)
    {:status 200
     :headers {"Content-Type" "application/json"}
     :body (generate-string {:message "Success"})}))

(defn handler [{:keys [uri] :as request}]
  (cond
    (= uri "/create-user")
    (create-user request)
    :else
    {:status 404
     :headers {"Content-Type" "application/json"}
     :body "Not Found"}))

(def allowed-origins [#"http://localhost:3000"])

(def allowed-methods [:get :post :put :delete])

(def allowed-headers #{:accept :content-type})

(def app
  (-> #'handler
      (wrap-defaults (assoc-in site-defaults [:security :anti-forgery] false))
      (wrap-cors :access-control-allow-origin allowed-origins
                               :access-control-allow-methods allowed-methods
                               :access-control-allow-headers allowed-headers)))


(defn start-server []
  (println "listening server")
  (jetty/run-jetty app {:port 4000}))

(println "starting server")
(start-server)

When I post to the create-user route the body is logged as

["^","~:fullname","myfullname","~:username","myusername","~:email","email@example.com","~:password","mypassword1234"]

How can I covert this into an object key map?

I would also like to both be able to inspect via priniting to the terminal the contents of the body which can likely be a nested map

For more infor this is what my clojurescript front end request looks like

(defn post-save-new-user [data] 
  (ajax/POST "http://localhost:4000/create-user"
    {:handler handle-response
     :error-handler handle-error
     :headers {"Accept" "application/json"
               "Content-Type" "application/json"}
     :params data}))

I have tried adding the following ring middlewares wrap-params, wrap-json-body and wrap-json-params but none has helped.

I have also tried the steps on this answer

  • You don't need to use `(slurp ...)` – Alan Thompson Jun 07 '23 at 14:30
  • Given the logged body, the request data was serialized with Transit but was deserialized using JSON. Perhaps the request has wrong `Content-Type` or perhaps there an issue in the implementation of `wrap-defaults` (no clue what it does so can't say more). – Eugene Pakhomov Jun 07 '23 at 14:32
  • Can you add the rest of the CLJS frontend code and sample data? – Alan Thompson Jun 07 '23 at 14:37
  • 2
    For things that "body-parser middleware" would do in node, we use `Muuntaja` which is a great library for this kind of content type negotiation. hth – Harold Jun 07 '23 at 15:33
  • First, you are simply reading the body as a string. You probably want to add middleware to parse it. Cheshire can parse the JSON for you, but your data is Transit (application/transit+json), so you should probably have a look at [Cognitect's Transit](https://github.com/cognitect/transit-clj). Are you trying to use Ring directly and avoid premade frameworks? Frameworks like Reitit often have middleware for content negotiation and such. – Sardtok Jun 08 '23 at 11:10

1 Answers1

0

If you do not want to use a framework, or a pre-written middleware, you can use Cheshire to parse the string of the body you have slurped in your handler:

(ns questhero.core
  (:require
   [cheshire.core :refer [generate-string parse-string]]
   [clojure.pprint :refer [pprint]]
   [ring.adapter.jetty :as jetty]
   [ring.middleware.cors :refer [wrap-cors]]
   [ring.middleware.defaults :refer [wrap-defaults site-defaults]]
   [ring.util.request]
   [ring.util.io]))

(defn create-user [request]
  (let [body (slurp (:body request))
        body-params (parse-string body true)]
    (println "body" body)
    (pprint body-params)
    {:status 200
     :headers {"Content-Type" "application/json"}
     :body (generate-string {:message "Success"})}))

Note that here I have added parse-string to the referred functions of Cheshire, as well as pprint from the clojure.pprint namespace. The boolean passed to parse-string converts all the keys to keywords.

One thing to be mindful of, is that the body of the request is a Java InputStream, meaning it is stateful. If you slurp it, it will be consumed, and cannot be read again. This is one reason to consider using a middleware that only reads the body once. I'll give you an example here, but this does the most basic possible content negotiation. It will not work for non-JSON encoded data except for ordinary URL-encoded form parameters, that are handled by the default middlewares you already use and put in :form-params:

(defn wrap-json-body [handler]
  (fn [request]
    (if (= (ring.util.request.content-type request)
           "application/json"))
      (let [body-params (-> request
                            :body
                            slurp
                            (parse-string true)]
        (handler (assoc request :body-params body-params)))
      (handler request))

This will parse the body just like was done in the first example, and place the parameters parsed in :body-params in the request. If you want to treat query-parms and JSON bodies the same, you might want to exchange the handler call to:

(handler (-> request
             (update :params merge body-params)
             (assoc :body-params body-params)))

Now you can add this middleware to your app:

(def app
  (-> #'handler
      (wrap-json-body)
      (wrap-defaults (assoc-in site-defaults [:security :anti-forgery] false))
      (wrap-cors :access-control-allow-origin allowed-origins
                               :access-control-allow-methods allowed-methods
                               :access-control-allow-headers allowed-headers)))
Sardtok
  • 449
  • 7
  • 18