I think I'm doing things right but I can't get my EDN out of the :body input stream. The Ring and Compojure handler is this:
dependencies:
[ring.middleware.params :as params]
[ring.middleware.edn :as edn]; https://github.com/tailrecursion/ring-edn
Handler:
(defroutes ajax-example
(PUT "/ajax-example" r
(println r)
{:status 200
:headers {"Content-Type" "application/edn"}
:body "yo"}))
I wrap it like:
(def ajax-wrapped
(-> ajax-example
edn/wrap-edn-params
params/wrap-params))
The println'd response correctly reveals that it is EDN and the content length is correct based on the simple test map I send in, but the map itself is nowhere to be found, it is forever trapped in the input stream of the :body... how to get at it?
Here is the response println:
{:ssl-client-cert nil, :remote-addr 0:0:0:0:0:0:0:1, :params {}, :route-params {}, :headers {origin http://localhost:6542, host localhost:6542, user-agent Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36, content-type application/edn, cookie _ga=GA1.1.1354518981.1429622648; content-length 20, referer http://localhost:6542/, connection keep-alive, accept /, accept-language en-US,en;q=0.8,sq;q=0.6, accept-encoding gzip, deflate, sdch, cache-control max-age=0}, :server-port 6542, :content-length 20, :form-params {}, :query-params {}, :content-type application/edn, :character-encoding nil, :uri /ajax-example, :server-name localhost, :query-string nil, :body #, :edn-params nil, :scheme :http, :request-method :put}
The :body is not pasting correctly above, it looks like this:
[open corner bracket] HttpInput org.eclipse.jetty.server.HttpInput#5d969109 [end corner bracket]
The client-side code sent from the browser using cljs-ajax lib is:
(defn ajax-send
[]
(let [push {:adder1 2 :add2 3}]
(PUT "/ajax-example"
{:format :edn
:params push
:handler ajax-result
:error-handler error-handler})))
Here is the output of a test suggested by one of the answers:
hf.examples> ((-> ajax-example params/wrap-params edn/wrap-edn-params) (-> (mock/request :put "/ajax-example")
(mock/content-type "application/edn")
(mock/body "{:hello \"there\"}")))
{:status 200,
:headers {"Content-Type" "application/edn"},
:body
"{:remote-addr \"localhost\", :params {:hello \"there\"}, :route-params {}, :headers {\"content-length\" \"16\", \"content-type\" \"application/edn\", \"host\" \"localhost\"}, :server-port 80, :content-length 16, :form-params {}, :query-params {}, :content-type \"application/edn\", :uri \"/ajax-example\", :server-name \"localhost\", :query-string nil, :edn-params {:hello \"there\"}, :scheme :http, :request-method :put}"}
hf.examples>
I also tried this:
(defroutes ajax-example
(PUT "/ajax-example" r
{:status 200
:headers {"Content-Type" "application/edn"}
:body (pr-str (dissoc r :body))}))
Curl result independent of the front end:
curl -X PUT -H "Content-Type: application/edn" -d '{:name :barnabas}' http://localhost:6542/ajax-example
{:ssl-client-cert nil, :remote-addr "0:0:0:0:0:0:0:1", :params {}, :route-params {}, :headers {"host" "localhost:6542", "content-length" "17", "content-type" "application/edn", "user-agent" "curl/7.37.1", "accept" "*/*"}, :server-port 6542, :content-length 17, :content-type "application/edn", :character-encoding nil, :uri "/ajax-example", :server-name "localhost", :query-string nil, :edn-params nil, :scheme :http, :request-method
The content-length of 17 matches the number of characters in the map passed via Curl. But the edn-params is nil! Where is the content?
EDIT: As an answer to the updated question, the wrap-edn-params function consumes the body of the request by fully reading the :body InputStream. Compojure routes passes the request to each parameter handler until a non nil value is returned. In this case, whichever handler is passed to routes as the first handler will consume :body and there will be no :body value for the 2nd handler to consume, resulting in a nil body value read by wrap-edn-params.
The request that is being passed to the ring handler probably does not have its content-type set to edn. The wrap-edn-params function will only parse the edn if the request content-type is set to edn.
In addition the parsed edn parameters will only be placed in the :params and :edn-params keys of the request map by the wrap-edn-params function, and therefore :body should not be used to access the parsed edn.
(require '[ring.mock.request :as mock])
(require '[ring.middleware.edn :as edn])
((-> ajax-example params/wrap-params edn/wrap-edn-params) (-> (mock/request :put "/ajax-example")
(mock/content-type "application/edn")
(mock/body "{:hello \"there\"}")))
{:remote-addr "localhost",
:params {:hello "there"},
:route-params {},
:headers
{"content-length" "16",
"content-type" "application/edn",
"host" "localhost"},
:server-port 80,
:content-length 16,
:form-params {},
:query-params {},
:content-type "application/edn",
:uri "/ajax-example",
:server-name "localhost",
:query-string nil,
:body #<ByteArrayInputStream java.io.ByteArrayInputStream#171788d8>,
:edn-params {:hello "there"},
:scheme :http,
:request-method :put}
I have fixed this but I do not know why the problem exists. I had another independent route that also wrapped EDN middle ware. Here is the reproducible example:
(defroutes example-routes2
(PUT "/test-edn" r
(println r)
{:status 200
:headers {"Content-Type" "application/edn"}
:body (pr-str (str "request is: " r))}))
(defroutes ajax-example
(PUT "/ajax-example" r
(println r)
{:status 200
:headers {"Content-Type" "application/edn"}
:body (pr-str (str "request is: " r))}))
(def edn-routes
(-> example-routes2
edn/wrap-edn-params))
(def ajax-wrapped
(-> ajax-example
edn/wrap-edn-params))
;;combining all routes on this page into a single handler
(def example-routes (routes example-routes1 ajax-wrapped edn-routes))
Note that these two routes are essentially identical and are being wrapped identically. The one that fails to parse EDN into edn-params is the one listed second in the example-routes def!
curl -X PUT -H "Content-Type: application/edn" -d '{:name :barnabasss}' http://localhost:6542/test-edn
returns:
"request is: {:ssl-client-cert nil, :remote-addr \"0:0:0:0:0:0:0:1\", :params {}, :route-params {}, :headers {\"host\" \"localhost:6542\", \"content-length\" \"19\", \"content-type\" \"application/edn\", \"user-agent\" \"curl/7.37.1\", \"accept\" \"/\"}, :server-port 6542, :content-length 19, :content-type \"application/edn\", :character-encoding nil, :uri \"/test-edn\", :server-name \"localhost\", :query-string nil, :body #, :edn-params nil, :scheme :http, :request-method :put}"
curl -X PUT -H "Content-Type: application/edn" -d '{:name :barnabasss}' http://localhost:6542/ajax-example
returns:
"request is: {:ssl-client-cert nil, :remote-addr \"0:0:0:0:0:0:0:1\", :params {:name :barnabasss}, :route-params {}, :headers {\"host\" \"localhost:6542\", \"content-length\" \"19\", \"content-type\" \"application/edn\", \"user-agent\" \"curl/7.37.1\", \"accept\" \"/\"}, :server-port 6542, :content-length 19, :content-type \"application/edn\", :character-encoding nil, :uri \"/ajax-example\", :server-name \"localhost\", :query-string nil, :body #, :edn-params {:name :barnabasss}, :scheme :http, :request-method :put}"
Switching their order in the example-routes switches which one works. Can anyone explain?
Related
I have the following code to define my routes in Compojure:
(ns my-project.my-test
(:gen-class)
(:require
[my-test.template-views :refer :all]
[compojure.core :refer [defroutes GET POST context]]
[compojure.route :as route]
[org.httpkit.server :refer [run-server]]))
(defn wrap-request
[handler]
(fn [request]
(let [{remote-addr :remote-addr uri :uri scheme :scheme request-method :request-method} request]
(println (str "REQUEST: " request)))
(handler request)))
(defroutes app
(wrap-request
(GET "/" request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))}))
(wrap-request
(GET "/page1" request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))}))
(wrap-request
(GET "/page2" request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))}))
(wrap-request
(GET "/page3" request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))}))
(route/resources "/")
(route/not-found {:status 404
:headers {"Content-Type" "text/html"}
:body "<h1>Not Found</h1>"}))
That works but it seems like I should be able to simplify it like this:
(ns my-project.my-test
(:gen-class)
(:require
[my-test.template-views :refer :all]
[compojure.core :refer [defroutes GET POST context]]
[compojure.route :as route]
[org.httpkit.server :refer [run-server]]))
(defn wrap-request
[handler]
(fn [request]
(let [{remote-addr :remote-addr uri :uri scheme :scheme request-method :request-method} request]
(println (str "REQUEST: " request)))
(handler request)))
(defn wrap-template
[route]
(wrap-request
(GET route request
{:status 200
:headers {"Content-Type" "text/html"}
:body (template-body (:uri request))})))
(defroutes app
(map wrap-template ["/" "/page1" "/page2" "/page3"])
(route/resources "/")
(route/not-found {:status 404
:headers {"Content-Type" "text/html"}
:body "<h1>Not Found</h1>"}))
However, when I do, I get this error backtrace:
Sat Apr 24 22:38:33 MDT 2021 [worker-2] ERROR - GET /page2
java.lang.ClassCastException: class clojure.lang.LazySeq cannot be cast to class clojure.lang.IFn (clojure.lang.LazySeq and clojure.lang.IFn are in unnamed module of loader 'app')
at compojure.core$routing$fn__368163.invoke(core.clj:185)
at clojure.core$some.invokeStatic(core.clj:2705)
at clojure.core$some.invoke(core.clj:2696)
at compojure.core$routing.invokeStatic(core.clj:185)
at compojure.core$routing.doInvoke(core.clj:182)
at clojure.lang.RestFn.applyTo(RestFn.java:139)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$apply.invoke(core.clj:662)
at compojure.core$routes$fn__368167.invoke(core.clj:192)
at org.httpkit.server.HttpHandler.run(RingHandler.java:117)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at java.base/java.lang.Thread.run(Thread.java:834)
Is there something about using (map) that is wrong here?
routes (and thus defroutes) expects each argument to be a request handler function. A list of handlers is not a handler function; hence the error. Happily, there is a function to convert a list of handlers to a single handler: routes! Since it wants N separate arguments, rather than a single list, you will need apply as well. So:
(defroutes app
(apply routes (map wrap-template ["/" "/page1" "/page2" "/page3"]))
(route/resources "/")
(route/not-found {:status 404
:headers {"Content-Type" "text/html"}
:body "<h1>Not Found</h1>"}))
As an aside, I generally suggest not using defroutes, simply because it does not compose as easily as separate def + routes, and for beginners it leads to forgetting that anything but defroutes exists, when in fact most interesting servers will want to apply a function to some of their routes.
I am sending an index-handler on "/this-route":
(defn index-handler [req]
(assoc (resource-response "index.html" {:root "public"})
:headers {"Content-Type" "text/html; charset=UTF-8"}))
(def routes ["" {"/this-route" {:get index-handler}}]) ;; works
Which works fine.
But when I append anything to this-route, I'm unable to send index-handler even though I can still send a basic res/response:
(def routes ["" {"/this-route" {"" {:get index-handler} ;; doesn't work
"/something" {:get index-handler} ;; doesn't work
"/something-else" (res/response "some response") ;; works
}}])
I get the errors in the client side console showing an error in the index.html line, where I start my app:
<script type="text/javascript">myapp.system.go();</script>
and the error is "myapp is not defined."
Why might this be, and what am I doing wrong?
--- EDIT ---
This is the complete error in console:
Uncaught ReferenceError: myapp is not defined
at something-else:15
DevTools failed to load SourceMap: Could not load content for chrome-extension://gighmmpiobklfepjocnamgkkbiglidom/include.preload.js.map: HTTP error: status code 404, net::ERR_UNKNOWN_URL_SCHEME
Your routes work fine:
(let [routes ["" {"/this-route" {:get :some-handler}}]]
(is= (bidi/match-route routes "/this-route" :request-method :get)
{:handler :some-handler, :request-method :get}))
(let [routes ["" {"/this-route" {"" {:get :handler-1}
"/something" {:get :handler-2}
"/something-else" {:get :handler-3}}}]]
(is= (bidi/match-route routes "/this-route" :request-method :get)
{:handler :handler-1, :request-method :get})
(is= (bidi/match-route routes "/this-route/something" :request-method :get)
{:handler :handler-2, :request-method :get})
(is= (bidi/match-route routes "/this-route/something-else" :request-method :get)
{:handler :handler-3, :request-method :get}))
As cfrick said, you have somehow created a problem compiling and/or loading your code. That is where you need to look.
I have a page with a login form, and a server that accepts POST requests. Here is the server:
(ns clj_server.core
(:require [org.httpkit.server :refer [run-server]]
[compojure.core :refer [defroutes POST]]
[compojure.route :as route]
[ring.middleware.params :refer [wrap-params]]))
(defn printPostBody [request]
{:status 200
:headers {"Content-Type" "text/html"}
:body request})
(defroutes routes
(POST "/login" request (printPostBody request))
(route/not-found {:status 404 :body "<h1>Page not found</h1"}))
(def app (wrap-params routes))
(defn -main [& args]
(run-server app {:port 8000})
(println "Server started on port 8000"))
When I make a login request, this is printed out:
[:remote-addr "0:0:0:0:0:0:0:1"][:params {"username" "asdf", "password" "ghkj"}][:route-params {}][:headers {"origin" "http://localhost:3449", "host" "localhost:8000", "user-agent" "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.172 Safari/537.36 Vivaldi/2.5.1525.46", "content-type" "application/x-www-form-urlencoded", "content-length" "27", "referer" "http://localhost:3449/", "connection" "keep-alive", "upgrade-insecure-requests" "1", "accept" "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3", "accept-language" "en-US,en;q=0.9", "accept-encoding" "gzip, deflate, br", "cache-control" "max-age=0"}][:async-channel #object[org.httpkit.server.AsyncChannel 0x2212125a "/0:0:0:0:0:0:0:1:8000<->/0:0:0:0:0:0:0:1:50592"]][:server-port 8000][:content-length 27][:form-params {"username" "asdf", "password" "ghkj"}][:compojure/route [:post "/login"]][:websocket? false][:query-params {}][:content-type "application/x-www-form-urlencoded"][:character-encoding "utf8"][:uri "/login"][:server-name "localhost"][:query-string nil][:body #object[org.httpkit.BytesInputStream 0x4e67c6c0 "BytesInputStream[len=27]"]][:scheme :http][:request-method :post]
So I'm wondering, what kind of data structure is it? It doesn't look like a hash map, yet when I print out (:params request) instead of request, I get
["username" "asdf"]["password" "ghkj"]
Is it a hash-map of a list of vectors? I don't understand what kind of data structure I'm dealing with here.
Also, why is {"username" "asdf", "password" "ghkj"} being converted into ["username" "asdf"]["password" "ghkj"] when I only ask for the params instead of the whole request?
I then tried printing out (get (:params request) "username") and I got "asdf". Which makes sense, but how is it allowing me to use get on a collection of multiple vectors?
Finally, how would I process JSON in my post requests? Is it just the same thing, or would I have to handle it differently?
What's the correct way to send a POST request with content-type application/json?
When I send the request to my application, I can't retrive the parameters.
curl -X POST http://localhost:3000/agents --data '{"name":"verma"}' --header "Content-type:application/json"
I'm using:
[org.clojure/clojure "1.8.0"]
[compojure "1.5.1"]
[ring/ring-defaults "0.2.1"]]
And my handler.cljs is:
▾| 1 (ns sof.rest.handler
| 2 (:require [compojure.core :refer :all]
| 3 [compojure.route :as route]
| 4 [clojure.data.json :as json]
| 8 [ring.middleware.defaults :refer [wrap-defaults api-defaults]]))
| 9
| 10 (defn json-response [data & [status]]
| 11 {:status (or status 200)
| 12 :headers {"Content-Type" "application/json"}
| 13 :body (json/write-str data)})
| 14
| 15 (defroutes app-routes
|~ 16 (POST "/agents" params (println params))
| 21
~| 22 (def app
~| 23 (wrap-defaults app-routes api-defaults))
Those are the params logged when I run the CURL command:
{:ssl-client-cert nil, :remote-addr 0:0:0:0:0:0:0:1, :params {}, :route-
params {}, :headers {accept */*, user-agent curl/7.54.0, content-type
application/json, content-length 16, host localhost:3000}, :server-port
3000, :content-length 16, :form-params {}, :compojure/route [:post
/agents], :query-params {}, :content-type application/json, :character-
encoding nil, :uri /agents, :server-name localhost, :query-string nil,
:body #object[org.eclipse.jetty.server.HttpInput 0x70504fbe
org.eclipse.jetty.server.HttpInput#70504fbe], :scheme :http, :request-
method :post}
You can slurp the :body input stream (read it into a string) and parse its JSON. Something like this:
(POST "/agents" request
(let [body (json/read-str (slurp (:body request)))]
(println body)))
But you should use the Ring middleware to do this automatically.
I know I can map the query string as a keyworkd map.
(defroutes my-routes
(GET "/" {params :query-params} params))
But is there a way do the same with a string keyed map?
(Using Compojure or Ring)
The point here is not to iterate over the map or use a function, but create it with string keys by default.
{ :a "b" } -> {"a" "b"}
Compojure 1.5.1 does not parse any query strings by default (by not using any middleware). However, this might have been different in earlier versions.
(require '[compojure.core :refer :all])
(require '[clojure.pprint :refer [pprint]])
(defroutes handler
(GET "/" x
(with-out-str (pprint x)))) ;; just a way to receive a pretty printed string response
$ curl localhost:3000/?a=b
{:ssl-client-cert nil,
:protocol "HTTP/1.1",
:remote-addr "127.0.0.1",
:params {}, ;; EMPTY!
:route-params {},
:headers
{"user-agent" "curl/7.47.1", "accept" "*/*", "host" "localhost:3000"},
:server-port 3000,
:content-length nil,
:compojure/route [:get "/"],
:content-type nil,
:character-encoding nil,
:uri "/",
:server-name "localhost",
:query-string "a=b", ;; UNPARSED QUERY STRING
:body
#object[org.eclipse.jetty.server.HttpInputOverHTTP 0x6756d3a3 "HttpInputOverHTTP#6756d3a3"],
:scheme :http,
:request-method :get}
Ring offers the ring.params.wrap-params middleware, which parses the query string and creates a hashmap of it under the params-key:
(defroutes handler
(wrap-params (GET "/" x
(prn-str (:params x)))))
$ curl localhost:3000/?a=55
{"a" "55"}
Additionaly ring.params.wrap-params can be used:
(defroutes handler
(wrap-params (wrap-keyword-params (GET "/" x
(prn-str (:params x))))))
$ curl localhost:3000/?a=55
{:a "55"}
Not sure about compojure, but you can undo it yourself:
(use 'clojure.walk)
(stringify-keys {:a 1 :b {:c {:d 2}}})
;=> {"a" 1, "b" {"c" {"d" 2}}}
https://clojuredocs.org/clojure.walk/stringify-keys