How exactly are POST request bodies processed in Clojure? (http-kit, compojure) - clojure

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?

Related

How do I simplify Compojure routes?

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.

Ring and Compojure - POST Requests with content-type application/json does not work

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.

Clojure, Compojure-api: Access Request headers

I am trying to implement request end point authentication. For that I want to access accessToken value from request headers.
My GET request end Point is
CURL Command
curl -X GET \
'http://localhost:3000/hello?id=10' \
-H 'accesskey: 23423sfsdfsdfsfg' \
-H 'cache-control: no-cache' \
-H 'content-type: application/json' \
-H 'postman-token: f69b34e6-4888-ec31-5fbc-b734e176571b' \
-d '{
"artwork": {id" : 1}
}'
HTTP Command
GET /hello?id=10 HTTP/1.1
Host: localhost:3000
Content-Type: application/json
accessKey: 23423sfsdfsdfsfg
Cache-Control: no-cache
Postman-Token: b974719d-5e1d-4d68-e910-e9ca50562b2f
My Code for GET Method Implementation
(defapi app
(GET ["/hello/:id", :id #"[0-9]+" ] [id]
(log/info "Function begins from here")
(def artworkData (logic/artwork-id (->> id (re-find #"\d+") Long/parseLong)))
(def data (if (not-empty artworkData)
{:data artworkData :status 200}
{:data [] :status 201}))
(ok data)))
I want to fetch accessKey: 23423sfsdfsdfsfg from request header.
Is there any way to get the value and use in my GET Method?
I am using POSTMAN to test all API end points.
Compojure has custom destructuring syntax (i.e., different from Clojure proper) for the parameters. You can bind the whole request map using keyword :as
(defapi app
(GET ["/hello/:id", :id #"[0-9]+" ] [id :as request]
If you want only request headers, the following should work
(defapi app
(GET ["/hello/:id", :id #"[0-9]+" ] [id :as {:headers headers}]
Note that this still allows you to bind path parameter id.
The Compojure Sweet API functions like [compojure.api.sweet :refer [defroutes GET PUT context]] let us bind the whole request or bind select headers. In the snippet below [:as request] makes the whole request available to me.
(GET
"/download/:id"
[:as request]
:header-params [{x-http-request-id :- X-Http-Request-Id nil}]
:path-params [id :- (describe String "The encoded id of the image")]
:summary "Download the image bytes"
:description "This endpoint responds 307 - Temporary Redirect to a cacheable presigned S3 URL for the actual bytes."
(let [http-response (->> request
walk/keywordize-keys
util/extract-base-url
(transform/generate-resource-url (util/decode-key id))
status/temporary-redirect)
expire-time (-> 3 hours from-now coerce/to-date ring-time/format-date)]
(log/infof "x-http-request-id is %s" x-http-request-id)
(response/header http-response "Expires" expire-time)))
The vector beginning :header-params [{x-http-request-id :- X-Http-Request-Id nil}] makes the value of the "X-HTTP-REQUEST-ID" header in the request available to my function directly as x-http-request-id.
The squiglies thing {...} makes the presence of x-http-request-id header optional in the request.
The :- X-Http-Request-Id nil stuff gives it a Schema which is defined somewhere else like (s/defschema X-Http-Request-Id (rss/describe String "Request ID for tracing calls")).
Once you've got those kids bound to names you just work with the names. The compojure folks don't do a great job at documenting everything you can do there. Poke around their examples and you'll find stuff like this.
I have figured out solution to the issue. Please check solution here.
(ns clojure-dauble-business-api.core
(:require [compojure.api.sweet :refer :all]
[ring.util.http-response :refer :all]
[clojure-dauble-business-api.logic :as logic]
[clojure.tools.logging :as log]
[clojure-dauble-business-api.domain.artwork]
[cheshire.core :as json])
(:import [clojure_dauble_business_api.domain.artwork Artwork]))
(defapi app
(GET ["/hello/:id", :id #"[0-9]+"] [id :as request]
(log/info "Function begins from here" request)
(def jsonString (json/generate-string (get-in request [:headers])))
(log/info "Create - Access Key is " (get-in (json/parse-string jsonString true) [:accesskey]))
(def artworkData (logic/artwork-id (->> id (re-find #"\d+") Long/parseLong)))
(def data (if (not-empty artworkData)
{:data artworkData :status 200}
{:data [] :status 201})))
I don't think it is smart way.
Can you anybody look into my solution and tell me Is there another way to get accesskey?

compojure wrap-json-body not working

I'm using the below code to try and access some json input in a PUT request however what I get returned has :body {}, I'm not sure what I'm doing wrong?
(ns compliant-rest.handler
(:use compojure.core ring.middleware.json)
(:require [compojure.handler :as handler]
[compojure.route :as route]
[ring.util.response :refer [response]]
[clojure.data.json :refer [json-str]]))
(defroutes app-routes
(PUT "/searches" {body :params} (response body))
(route/resources "/")
(route/not-found "Not Found"))
(def app
(-> (handler/site app-routes)
(wrap-json-body)
(wrap-json-response)))
(app {
:request-method :put
:uri "/searches"
:content-type "application/json"
:body (with-in-str (json-str {:field "value"}))
})
;; {:status 200, :headers {"Content-Type" "application/json; charset=utf-8"}, :body "{}"}
Also, I'm new to Clojure/Lisp, any comments about my syntax and style would be appreciated.
Two things stand out:
The unparsed request body is not supposed to be a string, but an InputStream. This means your test expression won't work as is.
wrap-json-body replaces (:body request) with a clojure data structure. It does not put anything in (:params request) or (:body (:params request)). You want wrap-json-params for that.
Thanks to Joost and the comments I found there is a ring function ring.util.io.string-input-stream that does what I mistakenly thought with-in-str did. Finally I had the following working:
(ns compliant-rest.handler
(:use compojure.core ring.middleware.json)
(:require [compojure.handler :as handler]
[compojure.route :as route]
[ring.util.response :refer [response]]
[ring.util.io :refer [string-input-stream]]
[clojure.data.json :refer [json-str]]))
(defroutes app-routes
(PUT "/searches/:id" {params :params body :body}
(response body))
(route/resources "/")
(route/not-found "Not Found"))
(def app
(-> (handler/site app-routes)
(wrap-json-body)
(wrap-json-response)))
;; Example request
(app {
:request-method :put
:uri "/searches/1"
:content-type "application/json"
:body (string-input-stream (json-str {:key1 "val1"}))
})
;; {:status 200, :headers {"Content-Type" "application/json; charset=utf-8"}, :body "{\"key1\":\"val1\"}"}
It's so awesome that I can just create a simple map and call my api's entry point without needing any sort of server or mocking. I'm totally being pulled into this whole dynamic languages thing with Clojure, the repl and light table!

How to set Content-Type header on Ring-Compojure application

I'm trying to get started with Clojure and Clojurescript by implementing a simple web app. Things are going pretty good so far and reading from different tutorials I've come up with the code below:
core.clj:
(ns myapp.core
(:require [compojure.core :as compojure]
[compojure.handler :as handler]
[compojure.route :as route]
[myapp.controller :as controller]))
(compojure/defroutes app-routes
(compojure/GET "/" [] controller/index)
(route/resources "/public")
(route/not-found "Not Found"))
(def app
(handler/site app-routes))
controller.clj:
(ns myapp.controller
(:use ring.util.response)
(:require [myapp.models :as model]
[myapp.templates :as template]))
(defn index
"Index page handler"
[req]
(->> (template/home-page (model/get-things)) response))
templates.clj:
(ns myapp.templates
(:use net.cgrand.enlive-html)
(:require [myapp.models :as model]))
(deftemplate home-page "index.html" [things]
[:li] (clone-for [thing things] (do->
(set-attr 'data-id (:id thing))
(content (:name thing)))))
The problem is I can't display non-ascii characters on the page and I don't know how to set HTTP headers on a page.
I see solutions like this but I simply can't figure out where place them in my code:
(defn app [request]
{:status 200
:headers {"Content-Type" "text/plain"}
:body "Hello World"})
P.S: Any suggestions about style and/or code organization are welcome.
Use ring.util.response:
(require '[ring.util.response :as r])
Then on your index function:
(defn index
"Index page handler"
[req]
(-> (r/response (->> (template/home-page (model/get-things)) response))
(r/header "Content-Type" "text/html; charset=utf-8")))
You can chain other actions on the response such as set-cookie and whatnot:
(defn index
"Index page handler"
[req]
(-> (r/response (->> (template/home-page (model/get-things)) response))
(r/header "Content-Type" "text/html; charset=utf-8")
(r/set-cookie "your-cookie-name"
"" {:max-age 1
:path "/"})))