Brad Lucas

A blog mostly about programming
May 18, 2018

Learning Ring And Building Echo

When you come to Clojure and want to build a web app you'll discover Ring almost immediately. Even if you use another library you are likely to find Ring in use.

What is Ring?

As stated in the Ring repository, it is a library that abstracts the details of HTTP into a simple API. It does this by turning HTTP requests into Clojure maps which can be inspected and modified by a handler which returns an HTTP response. The handlers are Clojure functions that you create. You are also responsible for creating the response. Ring connects your handler with the underlying web server and is responsible for taking the requests and calling your handler with the request map.

If you've had experience with Java Servlets you'll notice a pattern here but will quickly see how much simpler this is here.

Requests

Requests typically come from web browsers and can have a number of fields. Requests also have different types (GET, POST, etc), a unique URI with a query string, and message body. Ring takes all of this information and converts into a Clojure map.

Here is an example of a request map generated by Ring for a request to http://localhost:3000.

{:ssl-client-cert nil,
 :protocol "HTTP/1.1",
 :remote-addr "0:0:0:0:0:0:0:1",
 :headers
 {"cache-control" "max-age=0",
  "accept"
  "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
  "upgrade-insecure-requests" "1",
  "connection" "keep-alive",
  "user-agent"
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
  "host" "localhost:3000",
  "accept-encoding" "gzip, deflate, br",
  "accept-language" "en-US,en;q=0.9"},
 :server-port 3000,
 :content-length nil,
 :content-type nil,
 :character-encoding nil,
 :uri "/",
 :server-name "localhost",
 :query-string nil,
 :body "",
 :scheme :http,
 :request-method :get}
 

How did I get this map so I could show it here? From Echo, which is the application that I'll describe next.

Echo

When learning about Ring you'll hear all about the request map and you'll learn over time what fields it typically contains but sometimes you'll want to see it. Also, you may want to see what a script or form is posting to another system. In such a situation it can be very handy to point the script or form to a debugging site which shows what they are sending. This is the purpose of Echo.

To state it clearly. Echo is to be a web application that echoes back everything that is sent to it. It should take a request map and format it in such a way that it can be returned to the caller.

Steps

1. Start a new Clojure project

$ lein new echo

2. Add Ring dependencies

Add ring-core and ring-jetty-adapter to your project.clj file. Also, add a :main entry to echo.core so you can run your application.

  :dependencies [[org.clojure/clojure "1.8.0"]
                 [ring/ring-core "1.6.3"]
                 [ring/ring-jetty-adapter "1.6.3"]]
  :main echo.core

3. Create a handler and connect it to your Ring handler

Inside of core.clj' add a handler function and connect it to your adapter. Also add :gen-class and create a -main function so you can run` your application.

Here is the complete core.clj file. Notice that you are requiring ring.adapter.jetty. This is the adapter that represents the Jetty web server. It passes your handler requests.

Here the handler will return a minimal response with the words "Hello from Echo".

(ns echo.core
  (:require [ring.adapter.jetty :as jetty])
  (:gen-class))


(defn handler [request]
  {:status 200
   :header {"Content-Type" "text/plain"}
   :body "Hello from Echo"})


(defn -main []
  (jetty/run-jetty handler {:port 3000}))

At this point, you can test your app. From the root of your project enter the following to run it.

$ lein run

Then open a browser to http://localhost:3000/. You should see the following as a response.

Hello from Echo

4. Modify the handler to return the full request

Next, we'll modify the handler to return everything in the request. But, there are a couple things to figure out to make this work. First, you can't just send the request back else you'll get an error as the body of the request needs to be read.

To see what I mean modify your handler so it simply returns the request. The snippet looks like:

:body request

As a next step, you might want to pprint the request. You can try this by adding [clojure.pprint :as pprint] to your require clause and then calling pprint on the request and thinking it will go into the body by using the following snippet.

:body (pprint/pprint request)

Try that and watch the terminal where you entered lein run. You'll see the pprint output there.

Now that would be great if it was passed back to the browser. How? By capturing the output of pprint to a string and then passing that string to the browser through the :body field.

(defn handler [request]
  (let [s (with-out-str (pprint/pprint request))]
    {:status 200
     :header {"Content-Type" "text/plain"}
     :body   s
     }))

At this point go through and text with a new request from a browser. Here I see the following:

{:ssl-client-cert nil,
:protocol "HTTP/1.1",
 :remote-addr "0:0:0:0:0:0:0:1",
 :headers
 {"cache-control" "max-age=0",
  "accept"
  "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
  "upgrade-insecure-requests" "1",
  "connection" "keep-alive",
  "user-agent"
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
  "host" "localhost:3000",
  "accept-encoding" "gzip, deflate, br",
  "accept-language" "en-US,en;q=0.9"},
 :server-port 3000,
 :content-length nil,
 :content-type nil,
 :character-encoding nil,
 :uri "/",
 :server-name "localhost",
 :query-string nil,
 :body
 #object[org.eclipse.jetty.server.HttpInputOverHTTP 0x494e22ea "HttpInputOverHTTP@494e22ea"],
 :scheme :http,
 :request-method :get
 

There is one thing here that isn't a problem but will be when you try Echo with a POST from a form. It's the :body field. See how it is a HttpInputOverHTTP reference. This is something you want to read before sending so it shows up in the response. To do this see this final version of the handler.

(defn handler [request]
  (let [s (with-out-str (pprint/pprint (conj request {:body (slurp (:body request))})))]
    {:status 200
     :header {"Content-Type" "text/plain"}
     :body   s}))

Notice how the :body of the request was read with the slurp function and then the value of the :body field in the request is replaced with conj before being passed to pprint.

With a final test you should see something similar to the following:

{:ssl-client-cert nil,
 :protocol "HTTP/1.1",
 :remote-addr "0:0:0:0:0:0:0:1",
 :headers
 {"cache-control" "max-age=0",
  "accept"
  "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
  "upgrade-insecure-requests" "1",
  "connection" "keep-alive",
  "user-agent"
  "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
  "host" "localhost:3000",
  "accept-encoding" "gzip, deflate, br",
  "accept-language" "en-US,en;q=0.9"},
 :server-port 3000,
 :content-length nil,
 :content-type nil,
 :character-encoding nil,
 :uri "/",
 :server-name "localhost",
 :query-string nil,
 :body "",
 :scheme :http,
 :request-method :get}
 

Last test. Let's try posting something to our Echo.

$ curl --data 'firstname=Bob&lastname=Smith' http://localhost:3000/add/name?v=1

Here I see the following returned.

{:ssl-client-cert nil,
 :protocol "HTTP/1.1",
 :remote-addr "0:0:0:0:0:0:0:1",
 :headers
 {"user-agent" "curl/7.54.0",
  "host" "localhost:3000",
  "accept" "*/*",
  "content-length" "28",
  "content-type" "application/x-www-form-urlencoded"},
 :server-port 3000,
 :content-length 28,
 :content-type "application/x-www-form-urlencoded",
 :character-encoding nil,
 :uri "/add/name",
 :server-name "localhost",
 :query-string "v=1",
 :body "firstname=Bob&lastname=Smith",
 :scheme :http,
 :request-method :post}
 

Notice the body as well as the URI and query-string values. These are all pulled out of the request by Ring and as such, you can build handlers to look for values in these fields and respond accordingly.

Summation

At this point, you've created a very basic Ring application. Hopefully, it's one that you can use in the future to help debug other applications. Also, now that you see what fields are in requests you can build up from here.

Source code for the example Echo application is available at https://github.com/bradlucas/echo/tree/release/1.0.1.


Tags: ring clojure