Skip to content
Fenton Travers edited this page Jul 18, 2022 · 17 revisions

A web application developed for Ring consists of four components:

  • Handler
  • Request
  • Response
  • Middleware

Handlers

Handlers are functions that define your web application. Synchronous handlers take one argument, a map representing a HTTP request, and return a map representing the HTTP response.

For example:

(defn what-is-my-ip [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body (:remote-addr request)})

This function returns a map that Ring can translate into a HTTP response. The response returns a plain text file that contains the IP address that was used to access the web application.

Handlers may also be asynchronous. Handlers of this type take three arguments: the request map, a response callback and an exception callback.

For example:

(defn what-is-my-ip [request respond raise]
  (respond {:status 200
            :headers {"Content-Type" "text/plain"}
            :body (:remote-addr request)}))

Because these two types of handler have different arities, we can combine them into a handler that can be used synchronously or asynchronously:

(defn what-is-my-ip
  ([request]
    {:status 200
     :headers {"Content-Type" "text/plain"}
     :body (:remote-addr request)})
  ([request respond raise]
    (respond (what-is-my-ip request))))

All official Ring middleware supports both types of handler, but for most purposes synchronous handlers are sufficient.

The handler function can be converted into a web application through a variety of different methods which will be covered in the next section.

Requests

As previously mentioned, HTTP requests are represented by Clojure maps. There are a number of standard keys that will always exist, but requests can (and often do) contain custom keys added by middleware.

The standard keys are:

  • :server-port The port on which the request is being handled.

  • :server-name The resolved server name, or the server IP address.

  • :remote-addr The IP address of the client or the last proxy that sent the request.

  • :uri The request URI (the full path after the domain name).

  • :query-string The query string, if present.

  • :scheme The transport protocol, either :http or :https.

  • :request-method The HTTP request method, which is one of :get, :head, :options, :put, :post, or :delete.

  • :headers A Clojure map of lowercase header name strings to corresponding header value strings.

  • :body An InputStream for the request body, if present.

Previous versions of Ring also had the following keys. These are now DEPRECATED.

  • :content-type The MIME type of the request body, if known.

  • :content-length The number of bytes in the request body, if known.

  • :character-encoding The name of the character encoding used in the request body, if known.

Responses

The response map is created by the handler, and contains three keys:

  • :status The HTTP status code, such as 200, 302, 404 etc.

  • :headers A Clojure map of HTTP header names to header values. These values may either be strings, in which case one name/value header will be sent in the HTTP response, or a collection of strings, in which case a name/value header will be sent for each value.

  • :body A representation of the response body, if a response body is appropriate for the response's status code. The body can be one of four types:

    • String The body is sent directly to the client.

    • ISeq Each element of the seq is sent to the client as a string.

    • File The contents of the referenced file is sent to the client.

    • InputStream The contents of the stream is sent to the client. When the stream is exhausted, the stream is closed.

Middleware

Middleware are higher-level functions that add additional functionality to handlers. The first argument of a middleware function should be a handler, and its return value should be a new handler function that will call the original handler.

Here is a simple example:

(defn wrap-content-type [handler content-type]
  (fn [request]
    (let [response (handler request)]
      (assoc-in response [:headers "Content-Type"] content-type))))

This middleware function adds a "Content-Type" header to every response generated by the handler. It will only work with synchronous handlers, but we can extend it to support both synchronous and asynchronous handlers:

(defn content-type-response [response content-type]
  (assoc-in response [:headers "Content-Type"] content-type))

(defn wrap-content-type [handler content-type]
  (fn
    ([request]
      (-> (handler request) (content-type-response content-type)))
    ([request respond raise]
      (handler request #(respond (content-type-response % content-type)) raise))))

Notice that we factored out the common code that changes the response into its own function. By convention, if wrap-foo is our middleware function, then foo-request and foo-response are helper functions that operate on the request and response.

Once this middleware is written, it can be applied to a handler:

(def app
  (wrap-content-type handler "text/html"))

This defines a new handler, app that consists of the handler handler with the wrap-content-type middleware applied.

The threading macro (->) can be used to chain middleware together:

(def app
  (-> handler
      (wrap-content-type "text/html")
      (wrap-keyword-params)
      (wrap-params)))

Middleware is used often in Ring, and is used to provide much of its functionality beyond handling raw HTTP requests. Parameters, sessions, and file uploading are all handled by middleware in the Ring standard library.

Note: middleware runs from bottom to top, and if any middleware creates a response, that will short-circuit and the middleware above will not be called.

Next Page - Creating responses