Forcing www and https in Coast web application framework using a middleware

9 months ago

I recently ran into a problem where I wanted consistent URLs on my blog - meaning that no matter how you came to my site, or how you entered the URL to the address bar, you would be redirected to the URL variant that always uses www and https. Since I run this site on Heroku, which means I can't do any outside-of-the-app URL redirects that I could with the likes of Apache and Nginx, I needed to get a little creative.

This site is built with my favorite web framework for Clojure, Coast. It's a composed genius of many great Clojure libraries and has an active maintainer and a growing user base. As such, I wrote a middleware for Coast that redirects the site to the URL variant which uses www and https if either or isn't used.

Middlewares in Coast

If there's one thing Coast is lacking in, it's documentation, so before we get into the middleware I wrote, here's how to actually use middlewares in Coast. First of all let's create a file named middleware.clj in our root project directory which you can leave blank for now:

(ns middleware)

And then let's rewrite our core.clj, because the default documented version won't work with middlewares given how it defines routes:

(ns core
  (:require [coast]
            [routes]
  (:gen-class))

(def app (coast/app {:routes/site routes/routes}))

(defn -main [& [port]]
  (coast/server app {:port port
                     :max-body 100000000}))

(comment
  (-main))

This is the core.clj I use, you can take this as an example, but the important thing is to see that here we are including routes from its own namespace routes, and then instead of passing the routes as in the documentation of Coast which is {:routes routes} we pass {:routes/site routes/routes}. Now let's create a routes.clj file in the root directory of your project, the same place where we created the middleware.clj file, and in it let's put something like this:

(ns routes
  (:require [coast :refer [wrap-routes]]
            [coast.middleware.site :refer [wrap-layout]]
            [components :refer [layout]]
            [middleware :refer [redirect]]))

(def routes (wrap-routes redirect #(wrap-layout % layout)
                  [[:get "/" :components/about :about]
                   [:get "/blog" :components/blog :blog]
                   [:get "/blog/:slug" :components/blog-post]
                   [:get "/four-oh-four" :components/four-oh-four]]))

This is the actual routes file for this very blog, obviously you should change it to reflect the application you are building as this is just an example. Now as you can see it uses the Coast function wrap-routes which allows us to add middleware functions to our app requests before the actual component will load. You can see the redirect function there being the middleware this post is about, which is what we'll get to in a moment. It also uses the Coast function wrap-layout, which will wrap each component inside the layout component, which looks something like any other component you have written, with the exception that it has a second argument, body:

(defn layout [request body]
  [:html
   [:head
    [:body
     body]]])

And that is how you use middlewares with Coast.

Forcing www and https

Now as you saw the redirect function used above is the middleware that will force your site to redirect to www and https when either or is not being used. Open up your middleware.clj file that we created at the beginning of this post, and paste the following helper function in it:

(defn keywordize [map]
  (into {}
    (for [[k v] map]
      [(keyword k) v])))

We use this function to turn a vector into a key = value map, which will help us write the middleware, which is this function:

(defn redirect [handler]
  (fn [request]
    (let [uri-scheme (if (and (= :http (get request :scheme)) (not= :https (get-in request [:session :scheme]))) :http :https)
          uri-host (get (keywordize (get request :headers)) :host)
          localhost "localhost:1337"
          no-www? (when (nil? (re-find #"\www" uri-host)) true)
          no-https? (when (and (not= :post (get request :request-method)) (= :http uri-scheme)))]
      (if (or no-www? no-https?)
        (if-not (= localhost uri-host)
          (-> (coast/redirect (str "https://" (if (re-find #"\www" uri-host) uri-host (str "www." uri-host)) (get request :uri)))
              (assoc :session (conj {:scheme :https} (get request :session))))
          (handler request))
        (handler request)))))

Essentially what this does is it checks the no-www? and no-https? variable and if either or is not true, then it will check if we are in localhost or not by comparing the uri-host and localhost variables, which is defined as a string so feel free to change it according to your use case, and if we aren't, we redirect to the https://www variant of the URL. If however we are on localhost, or the no-www? and no-https? variable is nil, we pass the request to the handler and life goes on as normal.