Absolute Route URLs with Ring and Compojure - clojure

I need to redirect users to an absolute URL following oAuth authentication.
How do I construct an absolute URL for a Compojure route? Non-AJAX HTTP requests seem to omit the Origin header. Is there a Ring or Compojure helper function to build absolute URLs, or should I do this manually with the scheme and Host headers?
Lastly, and probably deserving of a separate question, are there helper functions in Compojure to generate route URLs based on the handler, ala Html.ActionLink(...) in MVC land?

The ring-headers project has a middleware that transforms relative to absolute urls:
(ns ring.middleware.absolute-redirects
"Middleware for correcting relative redirects so they adhere to the HTTP RFC."
(:require [ring.util.request :as req])
(:import [java.net URL MalformedURLException]))
(defn- url? [^String s]
(try (URL. s) true
(catch MalformedURLException _ false)))
(defn absolute-url [location request]
(if (url? location)
location
(let [url (URL. (req/request-url request))]
(str (URL. url location)))))

Related

How to use site-defaults middleware for one set of routes and api-defaults for another set of routes?

In my Compojure/Ring web application's handler, I need to serve one set of routes using the site-defaults middleware, and another separate set of routes using the api-defaults middleware. How can I do that?
The code below only serves one set of routes using the site-defaults middleware. What should I add to serve the second set of routes (api-routes) using the api-defaults middleware?
(web-experiment.handler
(:require [compojure.core :refer :all]
[compojure.route :as route]
[ring.middleware.defaults :refer [wrap-defaults
site-defaults
api-defaults]]
[web-experiment.views :refer :all]))
(defroutes app-routes
(GET "/" [] (index-page))
(GET "/about" [] (about-page))
(route/not-found "Not Found"))
(defroutes api-routes
(GET "/grapefruit" [:as {body :body}] (grapefruit-api body))
(GET "/factory" [:as {body :body}] (factory-api body))
(GET "/umbrella" [:as {body :body}] (umbrella-api body))
(route/not-found "Not Found"))
(def app
(wrap-defaults app-routes site-defaults))
;; TODO: Add api-routes. How to use api-defaults middleware to serve api-routes?
I've read these:
Serving app and api routes with different middleware using Ring and Compojure - Does not solve the problem because the solution presented does not work with the wrap-defaults middleware using the site-defaults configuration.
https://github.com/ring-clojure/ring-anti-forgery/pull/14 - Does not provide a clear solution (i.e. code snippet) to the problem I have.
You can just wrap some of your routes in one wrapper and some others in the other wrapper. You just need to organize your routes a bit differently.
Some time ago I wrote a demo what you can do with ring routes. You can find it at https://github.com/ska2342/ring-routes-demo/
In particular the part starting at line 70 of the demo should be interesting to you.
I'm not sure how this could be solved in Compojure, but you may wish to consider using Pedestal. This page provides a good introduction to the route matching process, which takes place before any interceptors are called (the Pedestal replacement for Ring middleware).
So you could define two different sets of middleware:
(def user-intc-chain [inject-connection auth-required (body-params/body-params)] )
(def api-intc-chain [inject-connection auth-required api-params-intc] )
and then define routes like:
["/echo" :get (conj user-intc-chain echo-intc) ] ; a "user" route
["/alive?" :get (conj api-intc-chain alive-intc) ] ; an "api" route
In addition to the built-in Pedestal features, I have a number of helper & convenience functions documented here: https://cloojure.github.io/doc/tupelo/tupelo.pedestal.html

Unable to access form paramaters when using nested defroutes in Compojure

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

Ring solution to redirect missing trailing slash urls

The behavior I'm trying to implement using Ring routes is approximately described in this question.
Basically I have some URLs that end with trailing slashes, and I'm trying to create a middleware that will redirect from e.g example.com/foo to example.com/foo/ if and only if /foo/ is a valid URL and /foo is not.
I'm currently using this middleware
(defn good-response? [resp]
(and resp (not= (:status resp) 404)))
(defn wrap-slash [handler]
(fn [{:keys [uri] :as req}]
(let [resp (handler req)]
(if (or (good-response? resp) (.endsWith "/" uri))
resp
(let [added-slash (str uri "/")]
(if (good-response? (handler (assoc req :uri added-slash)))
(redirect added-slash)
resp))))))
Which does almost everything it should: It redirects from /foo to /foo/ iff /foo/ exists and /foo does not.
My concern is that this solution will call (handler req) at least twice - once on the request for /foo and again when the client requests the redirected URL. It's not a problem now, but I could imagine it being painful to double the response time for some slow page with hundreds of DB queries or some such thing.
Is there a way to simply check if a handler exists for a given URL, without calling that handler? Could we avoid the problem entirely by say, making (:body request) lazy?
There is no general way in ring to check for "is this a valid uri?" without calling the whole stack handler with that uri, since there is no central list of uris, but even then handlers can decline to handle a request for any reason at all, not just based on its uri.
I would probably go the other way around; for all handlers that actually need this behavior, make them catch the "unslashed" version too and redirect then if needed/useful.
Or use a separate handler/middleware if the url should ALWAYS end in a slash given some rules and let the redirect fail if it doesn't match. Either way the end user will get a 404, so who cares, really?
But the most specific handlers are usually in the best position to make the decision.
Oh, and you probably don't just forward a POST to some other URI.
I fixed your code.
(defn good-response? [resp]
(and resp (not= (:status resp) 404)))
(defn wrap-slash [handler]
(fn [{:keys [uri] :as req}]
(let [resp (handler req)]
(if (or (good-response? resp) (.endsWith uri "/"))
resp
(let [added-slash (str uri "/")]
(if (good-response? (handler (-> req
(assoc :uri added-slash)
(assoc :path-info added-slash))))
(redirect added-slash)
resp))))))
you need to change :path-info.

Compojure - how to return 404 when required params are not provided?

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.

compojure defroutes - route sometimes not recognized

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.