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!
Related
I'm unable to access form parameters from a POST request. I've tried every combination of middleware and config options I've seen in the docs, on SO, etc. (including the deprecated compojure/handler options) and I'm still unable to see the parameters. I'm sure I'm missing something very obvious, so any suggestions (no matter how slight) would be greatly appreciated.
Here's my latest attempt, wherein I try to use the site-defaults middleware and disable the anti-forgery/CSRF protection provided by default. (I know this is a bad idea.) However, when I try to view the page in question in a web browser, the browser tries to download the page, as if it were a file it wasn't capable of rendering. (Interestingly, the page is rendered as expected when using Curl.)
Here's the latest attempt:
(defroutes config-routes*
(POST "/config" request post-config-handler))
(def config-routes
(-> #'config-routes*
(basic-authentication/wrap-basic-authentication authenticated?)
(middleware-defaults/wrap-defaults (assoc middleware-defaults/site-defaults :security {:anti-forgery false}))))
Previous attempt:
(def config-routes
(-> #'config-routes*
(basic-authentication/wrap-basic-authentication authenticated?)
middleware-params/wrap-params))
UPDATE:
The parameters appear to be swallowed by the outer defroutes:
(defroutes app-routes
(ANY "*" [] api-routes)
(ANY "*" [] config-routes)
(route/not-found "Not Found"))
So, my question now becomes: How can I thread the parameters through to the nested defroutes?
My temporary solve is based on this solution, but Steffen Frank's is much simpler. I will try that and follow-up.
UPDATE 2:
In trying to implement the suggestions provided by both of the current answers, I'm running into a new issue: route matches are overeager. e.g. given the following, POSTs to /something fail with a 401 response because of the wrap-basic-authentication middleware in config-routes.
(defroutes api-routes*
(POST "/something" request post-somethings-handler))
(def api-routes
(-> #'api-routes*
(middleware-defaults/wrap-defaults middleware-defaults/api-defaults)
middleware-json/wrap-json-params
middleware-json/wrap-json-response))
(defroutes config-routes*
(GET "/config" request get-config-handler)
(POST "/config" request post-config-handler))
(def config-routes
(-> #'config-routes*
(basic-authentication/wrap-basic-authentication authenticated?)
middleware-params/wrap-params))
(defroutes app-routes
config-routes
api-routes
(route/not-found "Not Found"))
(def app app-routes)
The issue is that when you define your routes in this way:
(defroutes app-routes
(ANY "*" [] api-routes)
(ANY "*" [] config-routes)
(route/not-found "Not Found"))
then any request will be matched by api-routes as long as it returns non-nil response. Thus api-routes does not swallow your request params but rather stealing the whole request.
Instead you should define your app-routes as (preferred solution):
(defroutes app-routes
api-routes
config-routes
(route/not-found "Not Found"))
or make sure that your api-routes returns nil for unmatched URL path (e.g. it shouldn't have not-found route defined).
Just a guess, but have you tried this:
(defroutes app-routes
api-routes
config-routes
(route/not-found "Not Found"))
You may find the following post useful. It talks about mixing api and app routes such that they don't interfere with each other and you avoid adding middleware from one to the toher etc. Serving app and api routes with different middleware using Ring and Compojure
How do I get the URL params into the request map in Pedestal? I am assuming that this needs the use of an interceptor? However the Pedestal documentation (or severe lack thereof) does not make this at all clear. Thanks.
Query parameters are parsed automatically by Pedestal, and the resulting map is placed in the request map under the :query-params key.
As a simple example, start with the pedestal-service template and use the following definitions:
(defn home-page
[request]
(ring-resp/response (format "Hello with params: %s" (:query-params request))))
(defroutes routes
[[["/" {:get home-page}]]])
Now if you browse to http://localhost:8080/?param=true&other=1234, you should see Hello world with paramters: {:param "true", :other "1234"}.
I'm struggling with understanding how to properly use sessions in Compojure/Ring.
Some of the examples I have come across:
https://github.com/brentonashworth/sandbar-examples/blob/master/sessions/src/sandbar/examples/session_demo.clj
http://rjevans.net/post/2628238502/session-support-in-compojure-ring
https://github.com/ring-clojure/ring/wiki/Sessions
These examples do not help me understand how to integrate sessions into something like a login mechanism.
(defroutes main-routes
(POST "/login" request (views/login request)))
;; views.clj
(defn login
[request]
(let [{params :params} request
{username :username} params
{password :password} params
{session :session} request]
(if (db/valid-user? username password)
(-> (logged-in request)
(assoc-in [:session :username] username))
(not-logged-in))))
I realize that this isn't correct as logged-in returns hiccup/html and I believe that the ring response map isn't added on until after the route is fully evaluated. This seems to be why all of the above examples show sessions being added to a complete response map. But, one of the features of Compojure to begin with was abstracting away the requirement of the development having to work with the response map. Therefore I feel like I must me missing something.
What would the correct way be to do the above?
If (logged-in request) returns the contents that should be rendered, then instead of associating :session :username onto the results of logged-in, you can return a proper response map:
{:body (logged-in request)
:session (assoc session :username username)}
:status, :headers, etc. have decent defaults if you do not provide them.
Suppose I have this handler:
(defroutes routes
(DELETE "/books" [id] (delete-book id)))
What can I do to make this app return HTTP 404 when request does not contain ID?
Firstly, you could make the id a part of the URI, which seems nice and RESTful and would allow you to use the route syntax to impose your condition:
(GET ["/books/:id" :id #"[0-9]+"] [] ...)
If you do prefer to use a parameter, something like
(if-not id
(ring.util.response/not-found body-for-404)
...)
should work in the next Ring version, though this particular function has not been released yet (it simply returns {:status 404 :headers {} :body the-body} though).
Also,
(when id
...)
would result in the equivalent of a route match failure and the remaining routes would be tried; then you could use
(compojure.route/not-found body-for-404)
as the final route which would always match.
Finally, if you wish to apply filtering to a large group of Compojure handlers, you may wish to combine them into a single handler with Compojure's defroutes or routes (the latter is a function) and wrapping them in a piece of middleware:
(defn wrap-404 [handler]
(fn wrap-404 [request]
(when (-> request :params :id)
(handler request))))
You can then include the wrapped handler as an entry in routes / defroutes forms.
I have a clojure / compojure webapp with the following routes
(defroutes my-routes
(GET "/app/preview" request (my-preview-function request))
(ANY "*" request (str "ANY page <br>" (request :params))))
The preview GET request is made with a couple of parameters. I find this works most of the time but sometimes the /ebook/preview is not found and processing drops to the ANY route, in which case the output is similar to this,
ANY page
{:* "/app/preview", :section "50", :id "48"}
Can anyone suggest what might cause the /ebook/preview request to be skipped? It is definitely a GET request being made; the HTML does not have a POST for the /app/preview URL and to be doubly sure I added a POST route for /app/preview and that was not being hit.
JAR versions:
Clojure 1.2
compojure-0.6.2
ring-core-0.3.7
jetty-6.1.14
ring-jetty-adapter-0.3.1
ring-servlet-0.3.1jar
servlet-api-2.5-6.1.14
Routes are wrapped as follows
(require '[compojure.handler :as handler])
(defn wrap-charset [handler charset]
(fn [request]
(if-let [response (handler request)]
(if-let [content-type (get-in response [:headers "Content-Type"])]
(if (.contains content-type "charset")
response
(assoc-in response
[:headers "Content-Type"]
(str content-type "; charset=" charset)))
response))))
(def app (-> my-routes
handler/site
wrap-stateful-session
(wrap-charset "utf-8")
(wrap-file "public")))
(defn run []
(run-jetty (var app) {:join? false :port 8080}))
If you're trying to figure out what request is causing the problems, stop throwing away the request map with (request :params) and just have a look at request. That will give you a map with all the information Compojure has; you can inspect it, and pass it back into your routes later to observe what happens (after you make some changes, say).
If
(my-preview-function request)
returns nil, then the routing will try the next route. Take a look at (source GET) and see how it matches (or doesn't) your route.