post-redirect? not working in liberator or Preflight response is not successful - clojure

I have the following code:
defresource handle-sign-in [redirect-uri]
:available-media-types ["application/json"]
:allowed-methods [:post]
:post! (prn "welcome to post")
:post-redirect? (fn [_] ;;(ring-response
{:location redirect-uri}
;;)
)
)
When I send the request, I get the errors Preflight response is not successful and XMLHttpRequest cannot load [authorize-uri] due to access control checks.
When I wrap the redirect location map around ring-response, however, I don't get the errors but neither do I get the redirect in the browser. What am I doing wrong?
-- EDIT --
This is my system.components config.
(defn config []
{:http-port (Integer. (or (env :port) 5000))
:middleware [[wrap-defaults api-defaults]
wrap-with-logger
wrap-gzip
ignore-trailing-slash
[wrap-reload {:dir "../../src"}]
[wrap-trace :header :ui]
wrap-params
wrap-keyword-params
wrap-cookies
[wrap-cors #".*"]
]})
and this is what my new resource looks like:
(defresource handle-sign-in [authorize-uri]
:available-media-types ["application/json"]
:allowed-methods [:post]
:post-redirect? true
:as-response (fn [d ctx]
(-> (as-response d ctx) ;; default implementation
(assoc-in [:headers "Access-Control-Allow-Origin"] "*")
(assoc-in [:headers "Access-Control-Allow-Headers"] "Content-Type")
)
)
:location authorize-uri
)
But I still get the "No 'Access-Control-Allow-Origin' header is present on the requested resource." error.

Please read about CORS. You'd need to either implement the OPTIONS method in the resource or wrap your handler with appropriate middleware, e.g. ring-cors.

Related

clojure: PUT to server always returns 404

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

wrap-cors middleware not working with system.components

I have the following system.components middleware config, in which I'm using the ring.middleware wrap-cors, to allow for redirects to an external server:
(defn config []
{:http-port (Integer. (or (env :port) 5000))
:middleware [[wrap-defaults api-defaults]
wrap-with-logger
wrap-gzip
ignore-trailing-slash
[wrap-reload {:dir "../../src"}]
[wrap-trace :header :ui]
wrap-params
wrap-keyword-params
wrap-cookies
[wrap-cors :access-control-allow-headers #{"accept"
"accept-encoding"
"accept-language"
"authorization"
"content-type"
"origin"}
:access-control-allow-origin [#"https://some-url"]
:access-control-allow-methods [:delete :get
:patch :post :put]]
]})
And this is supposed to insert headers into every response. But instead, on a request from the client which leads to a redirect to https://some-url, I get the following error in the client browser:
Access to XMLHttpRequest at 'https://someurl' (redirected from 'http://localhost:5000/some-uri') from origin 'http://localhost:5000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Why aren't the correct headers in the response despite adding the middleware?
-- EDIT --
I've also tried the [jumblerg.middleware.cors] wrap-cors middleware like so:
(defn config []
{:http-port (Integer. (or (env :port) 5000))
:middleware [[wrap-defaults api-defaults]
wrap-with-logger
wrap-gzip
ignore-trailing-slash
[wrap-reload {:dir "../../src"}]
[wrap-trace :header :ui]
wrap-params
wrap-keyword-params
wrap-cookies
[wrap-cors #".*"]
]})
And have added the headers using liberator like so:
(defresource some-route [redirect-uri]
:available-media-types ["application/json"]
:allowed-methods [:post]
:post-redirect? true
:as-response (fn [d ctx]
;; added headers
(-> (as-response d ctx)
(assoc-in [:headers "Access-Control-Allow-Origin"] "*")
(assoc-in [:headers "Access-Control-Allow-Headers"] "Content-Type")
)
)
;; redirect uri
:location redirect-uri
)
But still get the ````No 'Access-Control-Allow-Origin' header is present on the requested resource.``` error
Try this library to (wrap-cors):
[jumblerg/ring-cors "2.0.0"]
like this:
(wrap-cors your-routes identity)
Note the third parameter is a function to determine if an origin is allowed (or a list of reg exp)
You might have to add a manual route though:
(OPTIONS "/yourendpoint" req {:headers {"Access-Control-Allow-Headers" "*"}})

Clojure liberator library doesn't send the JSON response

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/

How do I mock a PUT request using ring.mock.request

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.

No 'Access-Control-Allow-Origin' header is present when origin is allowed in Pedestal

When I try and request a resource from a cljs app (running on http://localhost:3000) to my Pedestal server (running on http://localhost:8080) I get the below error. I would like to allow CORS from http://localhost:3000:
XMLHttpRequest cannot load http://localhost:8080/db/query. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:3000' is therefore not allowed access.
I am using cljs-http to send the request from the client. The request looks something like this:
(defn load-server-data
[]
(go
(let [q (<! (http/post "http://localhost:8080/db/query"
{:edn-params {:query '[:find ?rep ?last
:where
[?rep :sales-rep/first-name ?last]]}}))]
(println "q" q))))
The route for /db/query looks like this:
(defroutes routes
[[["/db"
{:post handlers/db-post}
["/query" {:post handlers/db-query}
^:interceptors [interceptors/edn-interceptor]]]]])
This is the handler for /db/query:
(defn db-query
[req]
(let [edn-params (:edn-params req)
q (:query edn-params)
args (:args edn-params)
q-result (apply d/q q (d/db conn) args)]
{:status 200
:body (pr-str q-result)}))
To run the server I execute this function in the REPL.
(defn run-dev
"The entry-point for 'lein run-dev'"
[& args]
(println "\nCreating your [DEV] server...")
(-> service/service
(merge {:env :dev
::server/join? false
::server/routes #(deref #'service/routes)
::server/allowed-origins {:creds true :allowed-origins (constantly true)}})
server/default-interceptors
server/dev-interceptors
server/create-server
server/start))
There does not seem to be much information around CORS for Pedestal. I have looked at the cors example but it seems to just work while mine does not. Is there another interceptor I need to add to my routes or some sort of configuration setting that I am missing here?
I have figured out the problem. It turns out that an error was being thrown, however, it was getting swallowed and hidden from my debugger. Simply adding a try catch around my handler function fixes the problem.
(defn db-query
[req]
(try
(let [edn-params (:edn-params req)
q (:query edn-params)
args (:args edn-params)
q-result (apply d/q q (d/db conn) args)]
{:status 200
:body (pr-str q-result)})
(catch Exception ex
{:status 400
:body "Not authorized"})))
My original response:
The purpose of CORS is to limit the origin of the requests. You have
to purposely tell it where requests can come from. This will fix it.
(def service {;other config stuff
io.pedestal.http/allowed-origins ["http://localhost:3000"]}
It appears this is a duplicate question. Apparently javascript ajax requests are by definition limited to single origin. That code would work in production only if the GET request is made by clj-http or http-kit on the ring server that spawn http://localhost:3000 and then a cljs-http ajax request is made to that same ring server on port 3000. I still don't know why your run-dev doesn't work, but if you're calling lein with run, this is definitely what's happening.