Skip to content
This repository

Aleph conforms to the Ring API for HTTP requests and responses, but allows for a response to be made on a different thread. Usually, a Ring handler looks something like this:

(defn handler [request]
  {:status 200
   :headers {"content-type" "text/plain"}
   :body "Hello World"})

The response for a request is returned by the handler function. However, this means that we are confined to a single thread the entire time. If we make a request to a database, we must hang the thread while waiting for the response. Aleph makes the response explicit, by giving the handler a channel into which it can send its response.

(use 'lamina.core)

(defn handler [response-channel request]
  (enqueue response-channel
    {:status 200
     :headers {"content-type" "text/plain"}
     :body "Hello World"}))

In this example, the handler responds immediately, but the response can be enqueued whenever you like. The channel will only accept a single message, so enqueue and enqueue-and-close can be used interchangeably.

Using Aleph with existing Ring handlers

It can be useful to have an asynchronous response, but often it’s more trouble than it’s worth. You’re encouraged to use Aleph synchronously wherever possible, and reserve asynchronous approaches for when they actually make your program simpler. Using an existing Ring handler within Aleph is easy:

(use 'aleph.http)

(defn handler [request]
  {:status 200
   :headers {"content-type" "text/plain"}
   :body "Hello World"})

(start-http-server (wrap-ring-handler handler) {:port 8080})

Note that the synchronous handler simply has been wrapped by wrap-ring-handler. This merges the channel and request together, putting the channel in the request hash under :channel. This allows easy interop with libraries that assume there will only be a single argument passed into the handlers. For instance, if we want to use Moustache for routing requests, the top-level handler must use wrap-ring-handler. But what if we want to have only some routes be synchronous? For that, we can use wrap-aleph-handler.

(use 'net.cgrand.moustache)

(defn async-handler [response-channel request]
  (enqueue response-channel
    {:status 200
     :headers {"content-type" "text/plain"}
     :body "async response"}))

(def handler 
  (app 
    ["sync"] {:get "response"}
    ["async"] {:get (wrap-aleph-handler async-handler)}))

(start-http-server (wrap-ring-handler handler) {:port 8080})

wrap-aleph-handler is meant to be used within the context of wrap-ring-handler. It splits the request and channel back apart, and returns a dummy response which will ignored by wrap-ring-handler.

For synchronous handlers, all Ring middleware will work as expected. For asynchronous handlers, middleware which alters the response will not work properly.

Streaming requests and responses

The distinction between synchronous and asynchronous responses is actually not all that clear; chunked responses allow for the headers to be returned immediately and the body to be asynchronously streamed. To create a streamed response, just create a response whose :body is a channel. The streamed response will end when the channel is closed.

This handler is synchronous, but still manages to asynchronously stream all numbers from 0 to 99.

(defn stream-numbers [ch]
  (future
    (dotimes [i 100]
      (enqueue ch (str i "\n")))
    (close ch)))

(defn handler [request]
   (let [stream (channel)]
     (stream-numbers stream)
     {:status 200
      :headers {"content-type" "text/plain"}
      :body stream}))

(start-http-server (wrap-ring-handler handler) {:port 8080})

Streamed requests will have a channel as their body. To check for this, see if (channel? body) returns true.

HTTP clients

Making a basic HTTP request is simple:

(http-request {:method :get, :url "http://example.com"})

The arguments in the request hash are the same taken by the request function in clj-http. This call will return an async-promise which will emit the response, which is also structured per the Ring API.

To make the request synchronous, you can use sync-http-request, which takes the same arguments as the asynchronous version.

http-request closes the connection with the server once the response has been received. If you want to make multiple requests to the same server, you should use http-client instead.

(http-client {:url "http://example.com"})

This will return a function which can be passed request hashes. These requests will be merged with the original hash passed into http-client, so the original hash can also specify the default :method, or any other parameters. This function will return an async-promise which will emit the server’s response.

The function returned by http-client represents a persistent connection to the server. If the connection is lost, an attempt to reconnect will be automatically made. To close this connection, use lamina.connections/close-connection.

http-client will only send a new request to the server once it has received the previous response. To send requests as soon as they are made, you can instead use pipelined-http-client. This can speed up the rate of communication with the server, but make sure you’ve read about HTTP pipelining and understand the potential pitfalls.

Since HTTP requests return a result channel they may be handled asynchronously via lamina’s on-realized function from lamina.core. For example:

(on-realized (http-request {:url "http://example.net" :method :get})
  #(println "Success: " %)
  #(println "Fail: " %))
;=> Success:  {:status 302, :content-type nil, :headers {content-length 0, connection close, server BigIP, location http://www.iana.org/domains/example/}, :content-length 0, :character-encoding nil, :body nil}

Alternatively, the client may be used in a blocking fashion such as:

@(http-request {:url "http://example.net" :method :get})
;=> {:status 302, :content-type nil, :headers {"content-length" "0", "connection" "close", "server" "BigIP", "location" "http://www.iana.org/domains/example/"}, :content-length 0, :character-encoding nil, :body nil}
Something went wrong with that request. Please try again.