I'm making a toy API using the Yada library in Clojure. It searches a database for city names starting with the given characters and returns some info about it.
I want a URI of the form: /cities/:name?count=:count so for example /cities/ber?count=4 will return the top 4 matches. But I also want /cities/ber without the ?count= parameter to return a default number of results (say just the first).
I've defined my route and yada handler like this:
(defn city-search-fn
[ctx]
(let [name (get-in ctx [:parameters :path :name])
count (get-in ctx [:parameters :query :count] 1)]
(city->geoposition name count)))
(def cities (yada/handler (yada/resource
{:methods
{:get
{:parameters {:path {:name String}
:query {:count Long}}
:produces ["application/json"
"application/edn"]
:response city-search-fn}}})))
(def routes
[["/cities/" :name] cities])
(def server
(yada/listener routes {:port 30000}))
This works fine if I supply the ?count= query parameter:
$ curl -i 'http://localhost:30000/cities/ber?count=2'
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Length: 259
Content-Type: application/json
Vary: accept
Server: Aleph/0.4.4
Connection: Keep-Alive
Date: Mon, 09 Sep 2019 16:01:45 GMT
[{"name":"Berlin","state":"Berlin","countrycode":"DE","timezone":"Europe/Berlin","latitude":52.52437,"longitude":13.41053},{"name":"Berbera","state":"Woqooyi Galbeed","countrycode":"SO","timezone":"Africa/Mogadishu","latitude":10.43959,"longitude":45.01432}]
But I get status 400 ({:status 400, :errors ([:query {:error {:count missing-required-key}}])}) if I don't supply it:
$ curl -i 'http://localhost:30000/cities/ber'
HTTP/1.1 400 Bad Request
Content-Length: 77
Content-Type: text/plain;charset=utf-8
Server: Aleph/0.4.4
Connection: Keep-Alive
Date: Mon, 09 Sep 2019 16:06:56 GMT
{:status 400, :errors ([:query {:error {:count missing-required-key}}])}
The documentation of yada says it supports optional query parameters using the "schema" library. So I found in schema's documentation that there exists a schema.core/maybe function. I tried to modify my yada resource as follows:
:parameters {:path.....
:query (schema/maybe {:count Long})}
this doesn't work (same 400 error).
Then I tried:
:parameters {:path.....
:query {:count (schema/maybe Long)}}
this also didn't work.
So my question is: what is the correct way to have an optional query parameter in yada?
To answer my own question, digging more into Schema documentation, here is the correct way:
:parameters {:path.....
:query {(schema/optional-key :count) Long}}
The key itself needs to be marked as optional.
Related
I am trying to server html over a ring based api server path . however everytime i hit the endpoint , i am getting this wierd error
java.lang.IllegalArgumentException: No implementation of method: :write-body-to-stream of protocol: #'ring.core.protocols/StreamableResponseBody found for class: clojure.lang.PersistentArrayMap
at clojure.core$_cache_protocol_fn.invokeStatic(core_deftype.clj:583) ~[clojure-1.10.1.jar:?]
at clojure.core$_cache_protocol_fn.invoke(core_deftype.clj:575) ~[clojure-1.10.1.jar:?]
at ring.core.protocols$eval18503$fn__18504$G__18494__18513.invoke(protocols.clj:8) ~[?:?]
at ring.util.servlet$update_servlet_response.invokeStatic(servlet.clj:106) ~[?:?]
at ring.util.servlet$update_servlet_response.invoke(servlet.clj:91) ~[?:?]
at ring.util.servlet$update_servlet_response.invokeStatic(servlet.clj:95) ~[?:?]
at ring.util.servlet$update_servlet_response.invoke(servlet.clj:91) ~[?:?]
at ring.adapter.jetty$proxy_handler$fn__18623.invoke(jetty.clj:27) ~[?:?]
Here is what my api is returning.
{:status 200
:body (resource-response "index.html" {:root "public"})}
Whereas if i hit the index.html path directly it is accessible at this route
http://localhost:8080/index.html
You get the error No implementation of method: :write-body-to-stream of protocol: #'ring.core.protocols/StreamableResponseBody found for class: clojure.lang.PersistentArrayMap because as a body you return a PersistentArrayMap instead of something that can be encoded as the body of a Ring HTTP response.
resource-response already returns a full response map (a PersistentArrayMap):
(resource-response "index.html")
;; => {:status 200,
;; :headers
;; {"Content-Length" "0", "Last-Modified" "Mon, 16 Nov 2020 14:22:48 GMT"},
;; :body
;; #object[java.io.File 0x239d3777 "/index.html"]}
so no need to wrap it in {:status 200, :body ...} since it becomes {:status 200 :body {:status 200, ...}} which lead to that error. To fix it your API can directly return:
(resource-response "index.html" {:root "public"})
I am getting a ring stream response which I do not know how to deal with.
I passed in a parameters to my ajax POST and when it gets to the function in my compojure route, instead of being the original parameter I passed in, I get a ring stream response being
{:remote-addr 0:0:0:0:0:0:0:1,
:params nil,
:route-params nil,
:headers {origin http://localhost:3300
host localhost:3300
user-agent Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/55.0.2883.87 Chrome/55.0.2883.87 Safari/537.36
content-type application/transit+json
content-length 42
referer http://localhost:3300/view
connection keep-alive
accept application/transit+json, application/transit+transit, application/json, text/plain, text/html, */*
accept-language en-GB,en-US;q=0.8,en;q=0.6
accept-encoding gzip, deflate, br}
:server-port 3300
:keep-alive? true
:uri /add-user!
:server-name ip6-localhost
:query-string nil
:body << stream: {:pending-puts 0
:drained? false
:buffer-size 42
:permanent? false
:type netty
:sink? true
:closed? true
:pending-takes 0
:buffer-capacity 16384
:connection {:local-address ip6-localhost/0:0:0:0:0:0:0:1:3300
:remote-address /0:0:0:0:0:0:0:1:34448
:writable? true
:readable? true
:closed? false
:direction :inbound}
:source? true} >>
:scheme :http
:request-method :post}
Why does this happen?
The body (where I believe my param lies) is
<< stream: {:pending-puts 0
:drained? false
:buffer-size 42
:permanent? false
:type netty
:sink? true
:closed? true
:pending-takes 0
:buffer-capacity 16384
:connection {:local-address ip6-localhost/0:0:0:0:0:0:0:1:3300
:remote-address /0:0:0:0:0:0:0:1:34448
:writable? true
:readable? true
:closed? false
:direction :inbound}
:source? true} >>
How do I deal with this to get my parameter out?
My parameter should be in the form {:id id :pass pass}
Thanks
Adding (wrap-params) middleware from ring.middleware.params when defining your application handler might help.
It should result in an accessible :params field that you can work with in your request handler.
I am playing with compojure-api and am blocked at trying to manage Content-Type for my simple webapp. What I want is to emit an HTTP response that is just plain/text, but somehow Compojure-API keeps setting it to "application/json".
(POST "/echo" []
:new-relic-name "/v1/echo"
:summary "info log the input message and echo it back"
:description nil
:return String
:form-params [message :- String]
(log/infof "/v1/echo message: %s" message)
(let [resp (-> (resp/response message)
(resp/status 200)
(resp/header "Content-Type" "text/plain"))]
(log/infof "response is %s" resp)
resp))
but curl shows the server responded Content-Type:application/json.
$ curl -X POST -i --header 'Content-Type: application/x-www-form-urlencoded' -d 'message=frickin compojure-api' 'http://localhost:8080/v1/echo'
HTTP/1.1 200 OK
Date: Fri, 13 Jan 2017 02:04:47 GMT
Content-Type: application/json; charset=utf-8
x-http-request-id: 669dee08-0c92-4fb4-867f-67ff08d7b72f
x-http-caller-id: UNKNOWN_CALLER
Content-Length: 23
Server: Jetty(9.2.10.v20150310)
My logging shows that the function requested "plain/text", but somehow the framework trumped it.
2017-01-12 18:04:47,581 INFO [qtp789647098-46]kthxbye.v1.api [669dee08-0c92-4fb4-867f-67ff08d7b72f] - response is {:status 200, :headers {"Content-Type" "text/plain"}, :body "frickin compojure-api"}
How do I gain control over Content-Type in a Compojure-API Ring application?
compojure-api serves response in format requested by HTTP client which is indicated using HTTP Accept header.
With curl you need to add:
-H "Accept: text/plain"
You can also provide a list of acceptable formats and the server will serve the response in the first supported format from that list:
-H "Accept: text/plain, text/html, application/xml, application/json, */*"
I never tried compojure so here goes nothing:
1.) your local val reps has the same name as the aliased namespace - kind of confusing
2.) to get access to the params - it seems - you have to apply ring.middleware.params/wrap-params to your routes
3.) ah yes the Content-Type: since you required :form-params, which didn't get delivered due to missing wrap-params you ended up in some sort of default route - hence not text/plain. Thats what I think happend, at least.
with
lein try compojure ring-server
demo/paste into repl:
(require '[compojure.core :refer :all])
(require '[ring.util.response :as resp])
(require '[ring.server.standalone :as server])
(require '[ring.middleware.params :refer [wrap-params]])
(def x
(POST "/echo" [message]
:summary "info log the input message and echo it back"
:description nil
:return String
:form-params [message :- String]
(let [resp (-> (resp/response (str "message: " message))
(resp/status 200)
(resp/header "Content-Type" "text/plain"))]
resp)))
(defroutes app (wrap-params x))
(server/serve app {:port 4042})
test:
curl -X POST -i --header 'Content-Type: application/x-www-form-urlencoded' -d 'message=frickin' 'http://localhost:4042/echo'
HTTP/1.1 200 OK
Date: Fri, 13 Jan 2017 17:32:03 GMT
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 14
Server: Jetty(7.6.13.v20130916)
message: frickin
I am trying to host static assets along with services in Pedestal 0.5.1. I am using the ::file-path to point to a directory to host the files. This works fine if I navigate directly to the file http://localhost:8888/index.html but if I go to the root of the site http://localhost:8888 it serves the files as application/octet-stream rather than text/html. I adapted the Hello World Sample and it has the same behavior.
src/hello_world/server.clj
(ns hello-world.server
(:require [io.pedestal.http :as http]
[io.pedestal.http.route :as route])
(:gen-class))
(def routes
(route/expand-routes [[]]))
(def service
{:env :prod
::http/join? false
::http/routes routes
::http/file-path "/tmp/www"
::http/type :jetty
::http/allowed-origins {:creds true :allowed-origins (constantly true)}
::http/port 8888})
(defonce runnable-service (http/create-server service))
(defn -main
"The entry-point for 'lein run'"
[& args]
(println "\nCreating your server...")
(http/start runnable-service))
Start lein run
$ curl -i localhost:8888
HTTP/1.1 200 OK
Date: Fri, 18 Nov 2016 16:02:56 GMT
Last-Modified: Fri, 18 Nov 2016 15:10:22 GMT
Content-Type: application/octet-stream
Content-Length: 12
Server: Jetty(9.3.8.v20160314)
hello world
$ curl -i localhost:8888/index.html
HTTP/1.1 200 OK
Date: Fri, 18 Nov 2016 16:03:02 GMT
Last-Modified: Fri, 18 Nov 2016 15:10:22 GMT
Content-Type: text/html
Content-Length: 12
Server: Jetty(9.3.8.v20160314)
hello world
Is there some way to fix the "/" route to serve the correct content-type?
To get correct content-types for files served as directory indexes,
add the interceptor io.pedestal.http.ring-middlewares/file-info to
your Pedestal configuration.
This requires that you override the default interceptor chain with
your own, so you will have to include all of the default interceptors
that your app needs.
For example, your service might look something like this:
(ns hello-world.service
(:require
[io.pedestal.http :as http]
[io.pedestal.http.ring-middlewares :as middlewares]
[io.pedestal.http.route :as route]
[io.pedestal.http.route.definition :refer [defroutes]]))
(defroutes routes
[[]])
(def service
{::http/type :jetty
::http/port 8080
::http/interceptors [http/log-request
http/not-found
middlewares/session
route/query-params
(middlewares/file-info) ; HERE
(middlewares/file "/tmp/www")
;; ... insert other interceptors ...
(route/router #(deref #'routes) :map-tree)]})
For examples of other default interceptors you might want to include,
see default-interceptors.
Explanation
This probably does not come up very often in practice because many web
applications use a handler function to generate the home page instead
of returning a static file.
For an alternate solution, you could write a route handler for the /
route which returns the contents of index.html with the appropriate
content-type.
Pedestal's default interceptor stack includes
io.pedestal.http.ring-middlewares/file
and io.pedestal.http.ring-middlewares/content-type.
These interceptors just wrap the Ring middleware functions
file-request
and content-type-response, respectively.
file-request returns a java.io.File object as the HTTP response.
content-type-response examines the request URI to determine the
value of the Content-Type header. Since the URI is just / it
defaults to application/octet-stream.
By contrast ring.middleware.file-info (which is deprecated) examines
path of the actual File object in the response.
See file-info-response.
io.pedestal.http.ring-middlewares/file-info is the interceptor
wrapper around ring.middleware.file-info/file-info-response.
According to Compojure:
Compojure does not serve static files by default, nor does it
automatically deal out 404s when no route matches.
So to deal with that, we have to set these Public Vars
files
not-found
resources
And this is how we currently set it:
(defroutes app-routes
(route/files "/" {:root "path/to/public"})
(route/resources "/")
(route/not-found "Not Found"))
It worked as expected when most of the static files are accessed through the web browser.
e.g.
http://localhost:3000/img/icon.png
But the problem is, it doesn't work on favicon files.
e.g.
http://localhost:3000/img/favicon.ico
It treats this as a different call which it should be serve as a static file.
Response to to the CURL I run:
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /img/favicon.ico HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:3000
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Tue, 28 Jul 2015 19:05:24 GMT
< Last-Modified: Thu, 28 May 2015 14:51:16 +0000
< Content-Length: 1106
< Content-Type: image/x-icon
* Server Jetty(7.6.13.v20130916) is not blacklisted
< Server: Jetty(7.6.13.v20130916)
files, resources, and not-found are functions, not variables that you set. When you call (route/files "/" {:root "path/to/public"}), a route for resolving URLs under "/" as static files under "path/to/public" is returned.
defroutes defines a collection of routes. These routes are tried in the order they are listed, until the first one that returns a response.
If you add a route (GET "/:slug" [* :as req slug] (search req slug)) before the others, then any URL other than "/" will be handled by this new route — including the favicon request. On the other hand, if you add it just before the not-found route, then it should work.
Also, if there isn't a static file that matches the request, then the files route will fail and the next one will be tried. So you should also check that favicon.ico actually exists and is in the img sub-directory.
umm...
This works for me, (wherever you define/declare your routes):
In the namespace section:
[ring.util.response :refer [resource-response]]
Down below (at least above (def app ):
(defn wrap-return-favicon [handler]
(fn [req]
(if (= [:get "/favicon.ico"] [(:request-method req) (:uri req)])
(resource-response "favicon.ico" {:root "public/img"})
(handler req))))
then in:
(def app
(-> routes
...
wrap-return-favicon
wrap-stacktrace))