I have a server hosting my API. My API relies on data requested from a third-party API (Spotify). Here are the relevant parts of my API handler:
(ns myapp.api.handler
(:require
[compojure.api.sweet :refer :all]
[ring.util.http-response :refer [ok forbidden no-content not-found bad-request]]
[clj-spotify.core :as spotify]))
(defroutes api-routes
(api
{:middleware [wrap-api]
:swagger {:ui "/api-docs"
:spec "/swagger.json"
:data {:info {:title "My API"
:description "A description for My API"}
:consumes ["application/json"]
:produces ["application/json"]}}}
(context "/api" []
(context "/me" []
(PUT "/player" []
:query-params [device_id :- String]
(handle-player-put device_id))))))
As you'll be able to tell from my route handler, I'd essentially like to forward the response of the third-party API to my API. Here is the handler function, handle-player-put:
(defn handle-player-put [device-id]
(let [available-devices (-> (spotify/get-current-users-available-devices
{}
(lm/oauth-token :spotify))
:devices)]
(doseq [device available-devices]
(when (= (:id device) device-id)
(if (not (:is_restricted device))
(let [response (spotify/transfer-current-users-playback
{:device_ids [device-id]
:play false}
(lm/oauth-token :spotify))]
(case (-> response :error :status)
nil (no-content)
404 (do
(println "Playback response: 404")
(not-found "Spotify could not find the requested resource."))
{:status (-> response :error :status)
:headers {}
:body (-> response :error :message)})))))))
After a successful (spotify/transfer-current-users-playback) request, response binds to {}. An example of a response after an error looks like {:error {:status 502, :message "Bad gateway."}}
No matter whether transfer-current-users-playback is successful or not, I always get a 404 error (with body text Not Found [404]). What am I doing wrong?
doseq always returns nil so your handler returns nil - which is interpreted by compojure as “this handler won’t handle the request; skip to the next handler” and if no other handler handles the request you get a 404 not found.
You should not use (doseq … (when … expr))) if you need to return expr
Related
I'm pretty new in the Clojure webdev ecosystem, I want to send a JSON response with the POST method using the liberator API, I tried this:
(POST "/post/savecomment"
request
(resource
:allowed-methods [:post]
:available-media-types ["application/json"]
:handle-ok (fn [ctx]
(format (str "{body: %s a: 1 b: 4}"), "the body part"))))
All looks fine, there is no error message, I get a "201 Created" response from ring, but the JSON data is not send, in Chrome "response" tab is just empty. Need I to add something? BTW, I'm using compojure, not compojure-api.
I also tried:
(POST "/post/savecomment" request (resource
:allowed-methods [:post]
:available-media-types ["application/json"]
:available-charsets ["utf-8"]
:handle-ok (fn [_] (rep/ring-response {:status 200 :body "\"this is json\""}))
:post! (fn [ctx] (rep/ring-response {:status 666 :body "\"this is json\""}))
))
But no luck.
For 201 Created responses you need to define the handler :handle-created, e.g.
(POST "/post/savecomment"
request
(resource
:allowed-methods [:post]
:available-media-types ["application/json"]
:handle-created (fn [ctx]
(format (str "{body: %s a: 1 b: 4}"), "the body part"))))
The tutorial covers the fundamental concepts of liberator: https://clojure-liberator.github.io/liberator/tutorial/
I just don't understand anything.
When I do curl -X GET --header 'Accept: application/json' 'http://localhost:3000/api/hello' from my swagger I get a nice answer {"result":1}.
However, when I try something like: http://localhost:3000/api/hello?criteria=drf, the server gives me java.lang.NullPointerException: Response map is nil. I am pretty sure it's not nil, since I still get my {"result":1} back on the screen.
But after that first error, every GET /hello request, without any query parameters, gives an error on server side, while still giving the right answer to the client?
And I need to restart server in order to make the error stop. Which works, until the first request with query params....
(ns swag.handler
(:require [compojure.api.sweet :refer :all]
[ring.util.http-response :refer :all]
[clojure.java.jdbc :as j]
[clojure.core.match :as match]
[clojure.spec.alpha :s s]
)
(def app
(api
{:swagger
{:ui "/"
:spec "/swagger.json"
:data {:info {:title "Trash"
:description "Compojure Api example"}
:tags [{:name "api", :description "some apis"}]}}}
(context "/api" []
:tags ["api"]
(GET "/plus" []
:return {:result Long}
:query-params [x :- Long, y :- Long]
:summary "adds two numbers together"
(ok {:result (+ x y)}))
(GET "/hello" []
:query-params [& z]
(let [criteria (:criteria z) values (:date z)]
(println z)
(ok {:result 1})))
))
)
If you're getting this message, check for favicon.ico request in the browser.
This may trigger a response map is null because no route is configured to handle it.
Issue for us was we needed trailing /s on the endpoint that was giving us this error (was looking at your q while we were debugging, but just solved).
How do I make this test pass:
(ns imp-rest.parser-test-rest
(:require [clojure.test :refer :all])
(:require [ring.mock.request :as mock] )
(:require [imp-rest.web :as w]))
(deftest test-parser-rest
(testing "put settings"
(w/app
(mock/request :put "/settings/coordinateName" "FOO" ))
(let [response (w/app (mock/request :get "/settings"))]
(println response )
(is (= (get (:body response) :coordinateName) "FOO")))))
it fails with:
FAIL in (test-parser-rest) (parser_test_rest.clj:30)
put settings
expected: (= (get (:body response) :coordinateName) "FOO")
actual: (not (= nil "FOO"))
Here's my handler:
(ns imp-rest.web
(:use compojure.core)
(:use ring.middleware.json-params)
(:require [clj-json.core :as json])
(:require [ring.util.response :as response])
(:require [compojure.route :as route])
(:require [imp-rest.settings :as s]))
(defn json-response [data & [status]]
{:status (or status 200)
:headers {"Content-Type" "application/json"}
:body (json/generate-string data)})
(defroutes handler
(GET "/settings" []
(json-response (s/get-settings)))
(GET "/settings/:id" [id]
(json-response (s/get-setting id)))
(PUT "/settings" [id value]
(json-response (s/put-setting id value)))
(route/not-found "Page not found") )
(def app
(-> handler
wrap-json-params))
which exposes this map (of settings):
(ns imp-rest.settings)
(def settings
(atom
{:coordinateName nil
:burnin nil
:nslices nil
:mrsd nil
}))
(defn get-settings []
#settings)
(defn get-setting [id]
(#settings (keyword id)))
(defn put-setting [id value]
(swap! settings assoc (keyword id) value)
value)
and the entry point:
(ns imp-rest.core
(:use ring.adapter.jetty)
(:require [imp-rest.web :as web]))
(defn -main
"Entry point"
[& args]
(do
(run-jetty #'web/app {:port 8080})
);END;do
);END: main
Now when I 'lein run' I can make a (working) request like this:
curl -X PUT -H "Content-Type: application/json" \
-d '{"id" : "coordinateName", "value" : "FOO"}' \
http://localhost:8080/settings
which is what I try to mock with the test. Any help appreciated.
If you want to have :id in your PUT /settings/:id route accepting body in format {"value": "..."}, you need to change your routes definition:
(defroutes handler
(GET "/settings" []
(json-response (s/get-settings)))
(GET "/settings/:id" [id]
(json-response (s/get-setting id)))
(PUT "/settings/:id" [id value]
(json-response (s/put-setting id value)))
(route/not-found "Page not found"))
And change how you call your PUT endpoint in the test:
(w/app
(-> (mock/request
:put
"/settings/coordinateName"
(json/generate-string {:value "FOO"}))
(mock/content-type "application/json")))
What was changed?
:id in your PUT URL route definition (/settings -> /settings/:id)
Your PUT request didn't send a correct request and content type.
If you want to have a PUT /settings route expecting {"id": "...", "value": "..."} request body, then you need to change how you create a mock request:
(w/app
(-> (mock/request
:put
"/settings"
(json/generate-string {:id "coordinateName" :value "FOO"}))
(mock/content-type "application/json"))
Your curl request specifies the parameters as JSON in the body of the PUT request, but your mock request tries to use URL parameters.
There are two options to resolve this:
compojure can automatically translate parameters, but only when the relevant middleware is present -- you have wrap-json-params added to your handler, but you're missing wrap-params. The answer from Piotrek Bzdyl amounts to making these params explicit in the compojure routes.
Alternatively, you can add the ID/value pair as JSON in the body of the mock request using request.mock.body.
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!
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 "/"})))