I am trying to integrate compojure-api (version 1.1.12) into an existing compojure-based application. While most things work, I am having an issue with request coercions on an existing REST call whose usage would be difficult to change at this point.
It's a POST
It expects parameters via multipart/form-data
Most parameters are optional.
Most parameters are simple: string or array of string.
One optional parameter is expected to be a JSON-encoded map.
I define the route like this:
(POST "/endpoint" request
:multipart-params
[required-strings :- (describe [s/Str] "Required, an array of strings"),
{optional-string :- (describe s/Str "An optional string") ""},
{others :- {s/Keyword s/Any} {}}]
...)
This works, unless I try to pass other-parameters in a request. For example, via curl:
curl -F "required-strings=[\"Hello\"]" -F "others={\"a\":1.0}" ...
This results in an invalid request (i.e. status 400) error with the content:
{"errors":{"others":"(not (map? a-clojure.lang.PersistentVector))"}}
I'm using ring-default's site-defaults, and I haven't modified the default coercions for the compojure api. I've traced the error to compojure.api.coerce/coerce. I can see the value that the coercer is working on, and it looks like:
{:required-strings "[\"Hello\"]"
:others "{\"a\":1.0}"}
On line 59 of coerce.clj, (coerce value) returns an error (per schema.utils/error?).
So, is it not possible to coerce a JSON-encoded multipart parameter to a Clojure map? I can define the parameter to be a string instead of a map, and do the parsing myself, but this defeats the purpose of using compojure-api and ring-swagger.
The coercer for others expects a clojure map not a string. To make it work you have to options.
First option: add wrap-json-params middleware and do application/json request instead of multipart/form-data:
curl ... -H 'Content-Type: application/json' \
-d $'{
"required-strings": ["Hello"],
"others": {"a": 1.0}
}'
Second option: add wrap-nested-params middleware and multipart/form-data request using nested param names:
curl ... -H 'Content-Type: multipart/form-data' \
-F "required-strings[]=Hello" \
-F "others[a]=1.0"
Related
I am using lacinia-pedestal for server and re-graph for client side clojuresript
My client code looks like
(re-frame/dispatch [::re-graph/init {:http-url
"http://localhost:8888/graphql"
:ws-url nil
:http-parameters {:headers {"Content-Type" "application/graphql"}
}}])
(re-frame/dispatch [::re-graph/query
"{current_user(token: ss) {id}}"
nil some-func])
However when I connect to my server, I see following error in console log
"message":"Failed to parse GraphQL query.","extensions":{"errors":[{"locations":[{"line":1,"column":null}],"message":"mismatched input '\"query\"' expecting {'query', 'mutation', 'subscription', '...', NameId}"}
Following curl request it works,
curl http://localhost:8888/graphql
-H 'Content-Type: application/graphql'
-d 'query {
current_user(token: "foo"){
id
}}'
Any help will be appreciated
I am attaching my network console log
re-graph uses application/json by default to send the query and variables as a JSON payload. You don't need to override the content-type in your init call.
There are three variants of format selection:
curl -uadmin:admin "http://localhost:8080/alfresco/service/hellouser.json"
curl -uadmin:admin "http://localhost:8080/alfresco/service/hellouser?format=json"
curl -uadmin:admin -H "Accept: text/html" "http://localhost:8080/alfresco/service/hellouser"
But this is unclear from the DOC what format would be selected for next query:
curl -uadmin:admin -H "Accept: text/html" "http://localhost:8080/alfresco/service/hellouser.xml?format=json"
I expect json here.
May someone provide links to relevant specifications or documentation which describes priority how {format} negotiated? like this is described for Rails:
Rails picks up the expected format from the query parameter format, or if not there from the URL path suffix, or it not there from the Accept header
UPD
The controller can handle all supplied formats: json, xml, html
UPD
Another corner case:
curl -uadmin:admin "http://localhost:8080/alfresco/service/hellouser.pl?format=json"
curl -uadmin:admin "http://localhost:8080/alfresco/service/hellouser.pl?format=xml"
I'd believe you wouldn't have a 200 response, only an error with content negotiation.
The code shows that:
?format=json(format_query_param) will be discarded by the .xml (format_suffix)
filter available renderers leaving only the XMLRenderer left
then it will loop on the accept header but none will match text/html
finally this will be down to the exception
I'm using swagger editor (version 2.10.5) to generate a flask api that uses custom headers and started to add the following line to each path:
parameters:
- $ref: '#/parameters/X-Forwarded-Host'
the relative definition:
X-Forwarded-Host:
name: 'X-Forwarded-Host'
in: header
description: Forwarded host header
required: true
type: string
Then running the auto-generated flask server
$ python3 -m swagger_server
creates some problems:
When making a curl request, headers are not right evaluated:
$ curl -X GET --header 'Accept: application/json' --header 'X-Forwarded-Host: example.com' http://localhost:8080
returns
health_get() missing required positional argument: 'X_Forwarded_Host'
Auto-generated tests are useless too:
headers = [('X_Forwarded_Host', 'X_Forwarded_Host_example'), ...
What am I doing wrong? Why is swagger-editor (or codegen) setting all "-" to "_"?
Thanks in advance
Ok, I figured out..
The problem was NOT with swagger-editor itself but how it generates the flask (Connexion) code.
Connexion request handling docs (url) says:
"Currently, header parameters are not passed to the handler functions as parameters. But they can be accessed through the underlying connexion.request.headers object which aliases the flask.request.headers object."
The solution is to remove all function attributes (related to headers) from the auto-generated controller and pick them from the request object, therefore:
From:
def health_get(X_Forwarded_Host):
...
To:
def health_get():
forwarded_host = connexion.request.headers['X-Forwarded-Host']
Bye!
I am trying to emulate this curl request
curl "https://{subdomain}.zendesk.com/api/v2/uploads.json?filename=myfile.dat&token={optional_token}" \
-v -u {email_address}:{password} \
-H "Content-Type: application/binary" \
--data-binary #file.dat -X POST
with the following code
(POST "/uploads" request
(let [filename (get-in request [:params "file" :filename])
file (get-in request [:params "file" :tempfile])
url (str "https://REDACTED.zendesk.com/api/v2/uploads.json?filename=" filename)]
(clj-http.client/post url {:headers {"Content-Type" “application/binary”}
:multipart-params [{:name "file"
:content file
:mime-type "application/binary”}]})
but I am getting a ‘422 Unprocessable Entity’ response from Zendesk. The file/tempfile is coming in as #object[java.io.File 0x3768306f "/var/folders/l3/7by17gp51sx2gb2ggykwl9zc0000gn/T/ring-multipart-6501654841068837352.tmp"] on the request.
I have played with clojure.java.io coercions (like clojure.java.io/output-stream) as mentioned at Saving an image form clj-http request to file, but that didn't help.
(PS. I’m fairly certain I don’t need to auth because I can get the direct upload to Zendesk to work through Postman.)
After revisiting this, the solution was simple. Zendesk expects the request body to be binary (as the curl request indicates). So, in this case, I passed the image to my server as base64 encoded data (just as JSON).
I then used this library to convert the base64 string to a byte array: https://github.com/xsc/base64-clj
(defn byte-array-from-base64
[base64-string]
(base64/decode-bytes (.getBytes base64-string)))
Finally, you can simple pass the byte array to Zendesk as the body of the clj-http library request.
(client/post
"https://REDACTED.zendesk.com/api/v2/uploads.jsonfilename=filename.jpg"
{:headers {"Authorization" "Basic AUTHORIZATION_TOKEN"
"Content-Type" "application/binary"}
:body (byte-array-from-base64 base64-string)})
I have a curl command that was given to me that I have to convert using requests.
curl --request POST "https://www.example.com" --data "user_id=200" --data "user_data=je93jfe92dj220,39fjid20djd93f302,93jfieheio02hfne,902jfoienfieshiu202" --header "Authorization: Bearer [TOKEN]"
using requests, the call should be
hdr = {'Content-Type': 'Content-type: application/json',
'Authorization': 'Bearer TOKEN' }
payload = {"user_id":200,"records":"je93jfe92dj220,39fjid20djd93f302,93jfieheio02hfne,902jfoienfieshiu202"
requests.post('https://www.example.com', headers=hdr, data=json.dumps(payload))
This isn't working as I'm getting an error returned that the 'user_id' param must be an integer. Not sure how to ensure that, as aren't all parameters formatted as strings when sent? The curl command does work, however.
Im not really sure the reason, but to make this work, just change data=json.dumps(payload) to json=json.dumps(payload). This worked just fine for me.