I want to create a multipart HTTP request using clj-http. Multipart request is below:
--Boundary
Content-Type: text/xml; charset=UTF-8
Content-Id id1
xml1
--Boundary
Content-Type: text/xml; charset=UTF-8
Content-Id id2
xml2
--Boundary--
I am using this Clojure code to build the multipart request:
(post "url"
{:multipart [{:name "XML1"
:content Xml1
:encoding "UTF-8"
:mime-type "text/xml"}
{:name "XML2"
:content Xml2
:encoding "UTF-8"
:mime-type "text/xml"}]})
How can I add Content-Id in the multipart?
:name attribute is used to give the name of entity i.e 1st content of multipart and so on.
Clojure is lacking this features to add the content id in multipart request.
However, in clojure, clj-http client internally uses the http-client api to build the multipart request. See this
link on how to create multipart in clj-http.
clj-http client is not using content id anywhere. So, one thing is clear, we can not create
multipart request with content-id.
One solution I found, just import http-client package in clojure and create multipart request. No need to download any http-client jar, as I told clj-http using http-client as dependency.
(:import (java.nio.charset Charset)
(org.apache.http.entity.mime MultipartEntity)
(org.apache.http.entity.mime FormBodyPart)
(org.apache.http.entity.mime HttpMultipartMode)
(org.apache.http.entity.mime.content
ByteArrayBody
FileBody
InputStreamBody
StringBody))
Just use below function template to create your own request.
And give multipart object as :body for http request.
But, it is not pure clojure implementation. It is temporary solution.
(defn build-form-body [formbody content cid]
(let [sb (StringBody. content "text/xml" (Charset/forName "utf-8"))]
(let [fb (FormBodyPart. formbody , sb)]
(.addField fb "Content-Id" cid)
fb)))
(defn build-multipart []
(let [mp-entity (MultipartEntity.)]
(.addPart mp-entity (make-form-body "formbody1" Xml1 "content-id1-val"))
(.addPart mp-entity (make-form-body "formbody2" Xml2 "content-id1-val2"))
mp-entity))
Note: give correct content type. In my case it is xml,so for me "text/xml" and string body.
If file then content type will change and use FileBody so on.
Hope it will help you.
If you treat the Content-Id as a header, add :Content-Id "Id 1" to your request map should help, change to :
{:name "XML1"
:content Xml1
:Content-Id "Id 1"
:encoding "UTF-8"
:mime-type "text/xml"}
If you treat Content-Id as body, just put it in to body
Related
I'm using the last available lacinia version: "0.36.0-alpha-3" with Luminus (Ring+reitit), but this version asks for a specific header:
$ curl 'http://localhost:3000/api/graphql' -X POST --data "{test_by_id(id: 5) { title } }" -H 'Content-Type: application/graphql'
that request works fine, but without "'Content-Type: application/graphql'" the request wouldn't work. So I need to define my re-graph init vector like:
[::re-graph/init
{:ws-url nil
:http-url "http://localhost:3000/api/graphql"
:http-parameters {:with-credentials? false
:headers {"Content-Type" "application/graphql"}
}
:ws-reconnect-timeout nil
:resume-subscriptions? false
:connection-init-payload {}}]
but putting that header makes re-graph unable to work properly:
{"errors":[{"message":"Failed to parse GraphQL query.","extensions":{"errors":[{"locations":[{"line":1,"column":null}],"message":"mismatched input '\"query\"' expecting {'query', 'mutation', 'subscription',
it looks like re-graph sends and receives data using "application/json" header, so lacinia asks for some type of header but re-graph can't work with that option.
I had the same problem, and I think I got a solution for it. re-frame requests follows the Apollo Specification, as stated by #aarkerio. Here is the code to keep the original endpoint working with the origina specification, and allow it to respond to re-frame requests. This will make the endpoint respond to Graphiql request (from your http://localhost:3000/graphiql route), and re-graph ones. Any comments or corrections are welcomed.
Replace the original function set on the /graphql route on src/clj/mem_learning/routes/services.clj:
["/graphql" {:post graphql-call}
Add the graphql-call function on that same file:
(defn graphql-call [req]
(let [body (:body-params req)
content-type (keyword (get-in req [:headers "content-type"]))]
(case content-type
:application/json (ok (graphql/execute-request-re-graph body))
:application/graphql (ok (graphql/execute-request (-> req :body slurp))))))
add the execute-request-re-graph to the src/clj/mem_learning/routes/services/graphql.clj file:
(defn execute-request-re-graph
"execute request with re-graph/apollo format"
[{:keys [variables query context]}]
(lacinia/execute compiled-schema query variables context)))
ANSWER:
It looks that Luminus creates a middleware configuration:
(defn service-routes []
["/api"
{:coercion spec-coercion/coercion
:muuntaja formats/instance
:swagger {:id ::api}
:middleware [;; query-params & form-params
parameters/parameters-middleware
;; content-negotiation
muuntaja/format-negotiate-middleware
;; encoding response body
muuntaja/format-response-middleware
;; exception handling
exception/exception-middleware
;; decoding request body
muuntaja/format-request-middleware
;; coercing response bodys
coercion/coerce-response-middleware
;; coercing request parameters
coercion/coerce-request-middleware
;; multipart
multipart/multipart-middleware
]}
commenting the line "muuntaja/format-negotiate-middleware" makes the "application/json" call possible.
SECOND UPDATE (four hours later)
Ok, that muuntaja middleware thing was not the problem at all, the real problem is that curl send the data with the format:
{ test_by_id(id: 7, archived: false) { title } }
meanwhile re-graph uses:
{"query":"query { test_by_id(id: 7, archived: false) { title } }","variables":null}
this is a normal java string btw not a data structure, so we need to do some changes, first a new function:
(defn graphql-call [req]
(let [body (-> req :body slurp)
full-query (json/read-str body :key-fn keyword)
_ (log/info (str ">>> **** full-query >>>>> " full-query))]
(ok (graphql/execute-request full-query))))
we set the function:
["/graphql" {:post graphql-call}]
and in my_app.routes.services.graphql file:
(defn execute-request [{:keys [variables query context]}]
(json/write-str (lacinia/execute compiled-schema query variables context)))
and now re-graph works!
(also now I can send and use variables in GraphQL)
It's necessary to set:
:http-parameters {:with-credentials? false
:oauth-token "ah4rdSecr3t"
:headers {"Content-Type" "application/graphql"}
btw. Also, maybe it's better:
(lacinia/execute compiled-schema query variables context)
than:
(json/write-str (lacinia/execute compiled-schema query variables context))
because it interferes with re-graph importing the data already as a native ClojureScript map.
I am writing a web app using Ring and Compojure, and Friend library for authorization. Recently I've made some changes to the project, and now I am not able to fetch request parameters any more.
What I used to do is this:
; Routes definition
(defroutes app-routes
(POST "/*.do" request
(insert-data (:params request))))
; Middlewares
(def secured-routes
(handler/site
(-> app-routes
wrap-params
(friend/authenticate friend-params-map))))
and form/URL parameters would be parsed into a Clojure map. Right now this does not seem to work, and (:params request) contains maps of the form
{:* <request-url>}
with a single :* key. If I try to (println request), a get a Clojure map with lots of key-value pairs, among which there are
:body #object[org.eclipse.jetty.server.HttpInputOverHTTP 0x6ef9a9e1 HttpInputOverHTTP#6ef9a9e1]
that seem to contain request data, correct? But how do I fetch those?
Previously I was just using wrap-params middleware as described above, and it worked.
One approach that works is invoking (body-string request) in each handler, that will return a stringified version of request body. But it would be great to get an automatically parsed Clojure map for each url handler. Obviously, I'm missing something.
Can someone please advise? I would also be happy to find out more how to debug those kinds of handler issues myself - meaning, debug middleware chains and find out why wrap-params does not get invoked or is invoked improperly.
Thanks!
UPDATE: per #nberger's comment, i've tried changing secured-routes definition to this:
(def secured-routes
(-> app-routes
(wrap-defaults site-defaults)
(friend/authenticate friend-params-map)))
but now the login page does not work at all (authentication stopped working). After moving wrap-defaults after friend/authenticate as suggested in the comment, Friend complains in big font about Invalid anti-forgery token (???)
I've found the issue. In the front-end code, POSTs to *.do have been modified to send application/json content-type. Below is the jQuery's ajax call that was used:
$.ajax({
url: '/saveInput.do',
type: 'POST',
contentType: 'application/json; charset=UTF-8',
data: JSON.stringify(formValues),
});
I've modified it to
$.ajax({
url: '/saveInput.do',
type: 'POST',
data: formValues,
});
and it works now. Moreover, original Ajax call with JSON content-type can be preserved by including wrap-json-params middleware from ring-json, so that routes definition finally becomes:
(def secured-routes
(-> app-routes
(friend/authenticate friend-params-map)
(wrap-defaults (assoc-in site-defaults [:security :anti-forgery] false))
wrap-json-params))
I've opted for the latter.
#nberger - thank you for help!
I am using compojure, cheshire and korma (and postgre db) for creating a rest service.
I've created a table with two string fields (name and description) with such structure:
(defentity posts
(pk :id)
(table :posts)
(entity-fields :name :description))
I can insert records into this table but when I try to exec
(defn get-all-posts []
(select posts))
and return the results from the server
defroutes app-routes
(GET "/" [] (get-start))
(context "/posts" []
(GET "/" [] (get-all-posts))
...
I receive such an error:
java.lang.IllegalArgumentException
No implementation of method: :render of protocol: #'compojure.response/Renderable found for class: clojure.lang.PersistentVector
As I see I need to convert posts collection to json. How to do it?
Ring responses can be either a map or a string. If they are a map then they use a few keys such as :status and :body to define the response and set cookies etc. You may want to explicitly convert your response from a Clojure sequence (edn) to JSON by wrapping the call to (get-all-posts) in generate-string (since you are using Cheshire) :
{:status 200
:content-type "application/json; charset=UTF-8"
:body (cheshire/generate-string (get-all-posts))}
And while you are at it it can't hurt to specify the content type and response code.
I am writing a small website in Clojure and Compojure. I would like to set the HTTP response status for each request based on the data found or not found.
The last call is the html5 macro that returns to the handler the html that needs to be sent back to the browser. Is it possible to set the HTTP response status somehow here?
(ns myapp.views.layout
(:require
[hiccup.page :refer (html5 include-css include-js)]))
(defn layout [title & content]
(html5
(head title)
(body content)))
If you only return text that text will be the body of the response. If you return a map, the map can describe other aspects of the response.
(defn layout [title & content]
{:status 200
:body (html5 (head title) (body content))})
If you return a map containing
{:status NNN
:body (my-code-here)}
then the contents of the :status key will be the http response code.
Just to add some details that might be helpful or interesting to others, each of the return values of your Compojure route handlers "... is treated intelligently" and that intelligence is encapsulated in the "compojure.response/render multimethod".
Based on a cursory examination of the render code, the reason why returning a map works is that the map you return is merge-d with the Ring response map that Compojure implicitly creates.
You might also want to include :headers {"Content-Type" "text/html"} (or whatever's appropriate) in the map for your handler return values. A Unicode character in the page title in my responses wasn't being rendered correctly because the content type header was missing.
i am trying to retrieve a website with Clojure and the clj-http library.
I wanted to start slow with a simple example:
(:body (client/get (str "http://www.google.com") {:as :clojure}))
As far as i understand the behaviour of the library, this call should return the body of the website but all it returns is <!doctype.
When i try to call
(:body (client/get (str "http://www.google.com") {:as :json}))
i even get an exception:
com.fasterxml.jackson.core.JsonParseException: Unexpected character ('<' (code 60))
I can not imagine the library to be broken but also i am not able to see an obvious error in my call. Has anybody of you experienced this behaviour?
To get the http response body as a string you can use the following:
(:body (client/get "http://www.google.com"))
The :as entry in the {:as :clojure} options is output-coercion, and is trying to convert the HTML body, from google.com, into a Clojure data structure. This will fail unless the response body actually contains Clojure code.
If you are trying to parse the HTML response, you might need to look into an additional library, like Enlive.
The problem is you're querying a URL that is not returning the data type you're coercing the result to.
For instance if you try with http://ip.jsontest.com/ this url which returns a proper json:
(require '[clj-http.client :as client])
(client/get "http://ip.jsontest.com/" {:as :json})
=> {:trace-redirects ["http://ip.jsontest.com/"], :request-time 1153,
:status 200,
:headers {"access-control-allow-origin" "*", "content-type" "application/json; charset=ISO-8859-1", "date" "Tue, 22 Oct 2013 19:50:36 GMT", "server" "Google Frontend", "cache-control" "private", "alternate-protocol" "80:quic,80:quic", "connection" "close"}, :body {:ip "186.54.233.167"}}
Response is properly parsed.
Checking the response body you can easily see there's a json indeed there:
(:body (client/get "http://ip.jsontest.com/"))
=> "{\"ip\": \"186.54.233.167\"}\n"