Asynchronous HTTP Client - Clojure - Documentation

Table of Contents

1 Quick start

If you just want to use it already.

1.1 Dependency

Declare dependency in your project.clj:

(defproject your-project "1.0.0-SNAPSHOT"
  :description "Your project description"
  :dependencies [[org.clojure/clojure "1.4.0"]
                 [http.async.client "0.5.2"]])

Make sure that your project depends on at least 1.3.0 Clojure as http.async.client will not run in earlier versions.

1.2 Require

Require it from your code:

(ns your.ns (:require [http.async.client :as http]))

1.3 GETting

To get HTTP resource:

(with-open [client (http/create-client)] ; Create client
  (let [response (http/GET client "http://github.com/neotyk/http.async.client/")] ; request http resource
    (-> response
        http/await     ; wait for response to be received
        http/string))) ; read body of response as string

2 Detailed start

2.1 Work modes

2.1.1 Asynchronous operations

When you do:

(http/GET <client> url)

Result will be a map of clojure.core/promises, and represents response.

Following HTTP methods have been covered so far:

For detailed description see HTTP methods.

You can submit options to HTTP methods as keyworded arguments, like this:

(http/GET <client> url :query {:key "value"})

Following options are supported:

:query
query parameters
:headers
custom headers to be sent out
:body
body to be sent, allowed only with PUT/POST
:cookies
cookies to be sent
:proxy
proxy to be used

For detailed usage of options see Request options.

Response map contains following keys:

:status
promise of lazy map of status fields
:code
response code
:msg
response message
:protocol
protocol with version
:major
major version of protocol
:minor
minor version of protocol
:headers
promise of lazy map of headers where header names are keyworded, like :server for example
:body
promise of response body, this is ByteArrayOutputStream, but you have convenience functions to convert it for example to string:
(http/string (http/GET <client> <url>))
:done
promise that is delivered once response receiving is done
:error
promise, if there was an error you will find Throwable here

2.1.2 Streaming

For consuming HTTP streams use:

(http/stream-seq <client> :get url)

Response here is same as in Asynchronous operations but :body will be lazy sequence of ByteArrayOutputStreams.

You can still use convenience functions like http/string for body, but remember that you are dealing now with seq.

For more details please see Lazy sequence.

2.1.3 Raw mode

This allows you to provide callbacks that will get triggered on HTTP response events like:

  • received status line,
  • received headers,
  • received body part,
  • completed request,
  • handle error.

All callbacks are expected to return tuple with first element been a value to be delivered for given response processing phase, second element is controlling execution and if you make it :abort than processing response is going to be terminated.

For detailed information on how to use this mode please see Low level.

2.2 HTTP methods

HTTP methods and convenience functions to request them.

2.2.1 GET

Most basic invocation of http.async.client/GET is only with url you want to get. Extended invocation includes options that can be any options accepted by http.async.client.request/prepare-request [:headers :query ..].

Simple invocation:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client "<your url>")
        status (http/status resp)
        headers (http/headers resp)]
    (println (:code status))
    (http/await resp)
    (println (http/string resp))))

Invocation with query parameters:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client "<your url>" :query {:param-name "some-value"})
        status (http/status resp)
        headers (http/headers resp)]
    (println (:code status))
    (http/await resp)
    (println (http/string resp))))

Invocation with proxy:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client "<your url>"
                       :query {:param-name "some-value"}
                       :proxy {:host host :port port})
        status (http/status resp)]
    (println (:code status))
    (http/await resp)
    (println (http/string resp))))

Invocation with cookies:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client "http://localhost:8123/cookie"
                       ;; Send cookie
                       :cookies #{{:domain "http://localhost:8123/"
                                   :name "sample-name"
                                   :value "sample-value"
                                   :path "/cookie"
                                   :max-age 10
                                   :secure false}})]
    (doseq [cookie (http/cookies resp)] ; Read cookies from server response
      (println "name:" (:name cookie) ", value:" (:value cookie)))))

Notice http.async.client/cookies function extracts cookies from response headers, so to start processing it you don't need to wait for whole response to arrive.

2.2.2 PUT/POST

http.async.client/PUT/http.async.client/POST work the same way as GET but they also accept :body argument.

:body can be:

  • String,
  • map, sent as form parameters,
  • vector, sent as multipart message,
  • input stream,
  • java.io.File, this will be sent using zero byte copy.
2.2.2.1 Submitting body as String

You can send String as body with PUT/POST:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/POST client "<your url>" :body "SampleBody")]
    ;; do something with resp
    ))
2.2.2.2 Submitting form parameters

Submitting form parameters is done via body map:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/POST client "<your url>" :body {:u "user" :p "s3cr3t"})]
    ;; do something with resp
    ))
2.2.2.3 Submitting multipart messages

To send multipart messages in body use vector of maps. Each map describes one multipart part.

Every map has to have :type key.

Following values for :type are recognized:

:string

Will send named string value in multipart part. Map spec:

:name
required, name of field sent in multipart part,
:value
required, value of field sent in multipart part,
:charset
optional, default UTF-8, charset used to encode value of field set in multipart part.

Example of string part in use:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/POST client "<your url>"
                        :body [{:type    :string
                                :name    "field-name"
                                :value   "field-value"
                                :charset "UTF-8" ; this is optional,
                                        ; and contains default value
                                }])]
    ;; do something with resp
    ))
:file

Will send named file in multipart part. Map spec:

:name
required, name of field sent in multipart part,
:file
required, java.io.File, whose contents will be sent as field value,
:mime-type
required, mime-type of Content-Type of this multipart part.
:charset
required, charset of Content-Type of this multipart part.

Example of file part in use:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/POST client "<your url>"
                        :body [{:type      :file
                                :name      "field-name"
                                :file      (File. "file/to/send.txt")
                                :mime-type "text/plain"
                                :charset   "UTF-8"}])]
    ;; do something with resp
    ))
:bytearray

Will send named byte array part.

Map spec:

:name
required, name of field sent in multipart part,
file-name
required, filename of Content-Disposition,
data
required, byte array containing data to sent as field value,
:mime-type
required, mime-type of Content-Type of this multipart part.
:charset
required, charset of Content-Type of this multipart part.

Example of :bytearray in use:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/POST client "<your url>"
                        :body [{:type      :bytearray
                                :name      "field-name"
                                :file-name "file-name.txt"
                                :data       (.getBytes "contents" "UTF-8")
                                :mime-type  "text/plain"
                                :charset    "UTF-8"}])]
    ;; do something with resp
    ))

To send request with multiple multiparts :body vector needs to contain multiple maps:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/POST client "<your url>"
                        :body [{:type    :string
                                :name    "field1-name"
                                :value   "field-value"
                                :charset "UTF-8" ; this is optional,
                                        ; and contains default value
                                }
                               {:type      :file
                                :name      "field2-name"
                                :file      (File. "file/to/send.txt")
                                :mime-type "text/plain"
                                :charset   "UTF-8"
                                }
                               {:type      :bytearray
                                :name      "field3-name"
                                :file-name "file-name.txt"
                                :data       (.getBytes "contents" "UTF-8")
                                :mime-type  "text/plain"
                                :charset    "UTF-8"}])]
    ;; do something with resp
    ))
2.2.2.4 Submitting body as InputStream

Another method to provide body is via InputStream:

(use '[clojure.java.io :only [input-stream]])
(with-open [client (http/create-client)] ; Create client
  (let [resp (http/PUT client "<your url>" :body (input-stream (.getBytes "SampleContent" "UTF-8")))]
    ;; do something with resp
    ))
2.2.2.5 Submitting body as File, a.k.a. zero byte copy

To use zero byte copy future, provide a File as :body

(import '(java.io File))
(with-open [client (http/create-client)] ; Create client
  (let [resp (http/PUT "<your url>" :body (File. "<path to file>"))]
    ;; do something with resp
    ))

2.2.3 DELETE

To call http.async.client/DELETE on a resource:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/DELETE "<your url>")]
    ;; do something with resp
    ))

2.2.4 HEAD

To call http.async.client/HEAD on a resource:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/HEAD "<your url>")]
    ;; do something with resp
    ))

2.2.5 OPTIONS

To call http.async.client/OPTIONS on a resource:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/OPTIONS "<your url>")]
    ;; do something with resp
    ))

2.3 Request options

Following options can be provided to requests and are defined by http.async.client.request/prepare-request:

:query
query parameters
:headers
custom headers to be sent out
:body
body to be sent, allowed only with PUT/POST
:cookies
cookies to be sent
:proxy
proxy to be used
:auth
authentication map
:timeout
timeout configuration

2.3.1 :query

Query parameters is a map of keywords and their values. You use it like so:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url :query {:key1 "value1" :key2 "value2"})]
    (http/await resp)
    (http/string resp)))

2.3.2 :headers

Custom headers can be submitted same way as :query:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url :headers {:header-name1 "value1"
                                            :header-name2 "value2"})]
    (http/await resp)
    (http/string resp)))

2.3.3 :body

Body can be provided with a message only with PUT/POST, it doesn't make sense to have body with other HTTP methods.

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/PUT client url :body "sample body")]
    (http/await resp)
    (http/string resp)))

:body can be:

  • String,
  • map, sent as form parameters,
  • vector, sent as multipart message,
  • input stream,
  • java.io.File, this will be sent using zero byte copy.

Please see PUT/POST for more documentation.

2.3.4 :cookies

Cookies can be provided to request as follows:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client "http://localhost:8123/cookie"
                       :cookies #{{:domain "http://localhost:8123/"
                                   :name "sample-name"
                                   :value "sample-value"
                                   :path "/cookie"
                                   :max-age 10
                                   :secure false}})]
    (http/await resp)
    (http/string resp)))

:cookies option takes sequence of cookie maps, in this example a hash set. Cookie map consist of:

:domain
Domain that cookie has been installed
:name
Cookie name
:value
Cookie value, note that there is no additional processing so you should encode it yourself if needed.
:path
Path on with cookie has been installed
:max-age
Max age that cookie was configured to live
:secure
If cookie is secure cookie

Cookie reading is described in Reading cookies.

2.3.5 :proxy

Proxy can be configured per request basis as follows:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url :proxy {:host h :port p})]
    (http/await resp)
    (http/string resp)))

Proxy expects a map with following keys:

:host
proxy host
:port
proxy port
:protocol
optional protocol to communicate with proxy. Can be :http (default) or :https.
:user
optional user name to use for proxy authentication. Must be provided with :password.
:password
optional password to use for proxy authentication. Must be provided with :user.

2.3.6 :auth

Authentication can be configured per request basis. For now BASIC and DIGEST methods are supported.

Basic method is default, so you don't have to specify it:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url :auth {:user u :password p})]
    ;; Check if response is not 401 or so and process response
    ))

Though you can:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url :auth {:type :basic :user u :password p})]
    ;; Check if response is not 401 or so and process response
    ))

And for digest method you will need realm as well:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url
                       :auth {:type :digest :user u :password p :realm r})]
    ;; Check if response is not 401 or so and process response
    ))

Controlling preemptive authentication behavior is also possible:

(with-open [client (http/create-client)]
  (let [resp (http/GET client url :auth {:user u :password p :preemptive true})]
    ;; process response
    ))

2.3.7 :timeout

Response timeout can be configured per request as well. Timeout value is time in milliseconds in which response has to be received. There is special value -1 that indicates infinite timeout.

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url :timeout -1)]
    (http/await resp)
    ;; process response
    ))

Sample above will wait until response is fully received, as long as it takes (-1 timeout).

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url :timeout 100)]
    (http/await resp)
    (if (http/failed? resp)
      ;; did not get response in configured timeout
      ;; process response
      )))

Example above configures timeout to 100ms, so await will only wait for 100ms, after that response is done. Which doesn't necessarily mean that it was delivered to client successfully, because it was restricted by timeout, that is why example contains check if response has failed.

2.4 Streaming

HTTP Stream is response with chunked content encoding. Those streams might not be meant to ever finish, see twitter.com streams, so collecting those responses as a whole is impossible, they should be processed by response parts (chunks) as they are been received.

Two ways of consuming a HTTP Stream are supported:

2.4.1 Lazy sequence

You can get HTTP Stream as lazy sequence of it's body. This is very convenient method as seq is native type of Clojure so you can apply all mapping, filtering or any other standard function that you like to it.

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/stream-seq client :get url)]
    (doseq [s (http/string resp)]
      (println s))))

stream-seq arguments:

http method
any of supported methods can be used, though it makes sense only to use :get, :put and :post
url
URL of HTTP resource
options
same as normal Request options.

It is important to understand that seqs returned by body or string (which in turn calls body) are backed by queue. One of consequences of it is that once you consumed some body parts they will not be available anymore. Let's see code speak for itself.

(let [resp (http/stream-seq :get url)]
  (println "1: " (first (http/string resp)))
  (println "2: " (first (http/string resp))))

Assuming that part1 is first chunk and part2 is second. This code will print following:

1: part1
2: part2

Second consequence of been directly backed by queue is that you can have multiple consumers of same response and non of them will get same body part.

And finally this implementation is not holding to it's head.

2.4.2 Call-back

Consuming HTTP Stream with call-back is quite straight forward with http.async.client. You will need to know what HTTP Method you will call, what URL and provide a call back function to handle body parts been received.

(with-open [client (http/create-client)] ; Create client
  (let [parts (ref #{})
        resp (http/request-stream client :get url
                                  (fn [state body]
                                    (dosync (alter parts conj (string body)))
                                    [body :continue]))]
    ;; do something to @parts
    ))

Few notes on implementing body part callback:

  • state is a map with :status and :headers as promises, at stage when you get called for body part, both of them should be in place already, though it is advised to use convenience methods to read them, see Reading status line and Reading headers,
  • call-back has to follow guidelines described in Body part,
  • some streams are not meant to be finished, in that case don't collect body parts, as for sure you will run out of available resources,
  • try not to do any heavy lifting in this callback, better send it to agent.

2.5 Response handling

http.async.client exposes some convenience functions for response handling.

2.5.1 Awaiting response

If you call any of Asynchronous operations, Streaming or Raw mode you actually asynchronously execute HTTP request. Some times you might need to wait for response processing to be done before proceeding, in order to do so you call http.async.client/await. It takes only one argument, that is response and returns once receiving has finished.

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url)]
    (http/await resp)))

Sample above will behave like synchronous HTTP operation. For convenience it returns same response so you can use it further, for example like that:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url)]
    (http/string (http/await resp))))

2.5.2 Reading status line

http.async.client/status returns status lazy map of response. It will wait until HTTP Status has been received.

(with-open [client (http/create-client)] ; Create client
  (let [resp   (http/GET client url)
        status (http/status resp)]
    (:code status)))

Sample above will return HTTP response status code, notice that after this returns headers and body, might not been delivered yet.

2.5.3 Is redirect?

http.async.client/redirect? checks if response is redirect.

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/await (http/GET client url))]
    (when (http/redirect? resp)
      (println "Response is redirect."))))

2.5.4 Reading headers

http.async.client/headers returns headers lazy map of response. It will wait until HTTP Headers are received.

(with-open [client (http/create-client)] ; Create client
  (let [resp    (http/GET client url)
        headers (http/headers resp)]
    (:server headers)))

Again, like in case of status, body might not have been delivered yet after this returns.

2.5.5 Reading content-type

http.async.client/content-type returns value of Content-Type header.

(with-open [client (http/create-client)] ; Create client
  (let [resp         (http/GET client url)
        content-type (http/content-type resp)]
    (println "Content-Type of response:" content-type)))

2.5.6 Reading location

http.async.client/location will return redirect target if response is redirect.

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url)
        loc  (http/location resp)]
    (println "Location of redirect:" loc)))

2.5.7 Reading cookies

http.async.client/cookies returns seq of maps representing cookies. It will wait until HTTP Headers are received.

(with-open [client (http/create-client)] ; Create client
  (let [resp    (http/GET client url)
        cookies (http/cookies resp)]
    (map :name cookies)))

Sample above will return sequence of cookie names that server has set.

2.5.8 Reading body

http.async.client/body returns either ByteArrayOutputStream or seq of it, depending if you used Asynchronous operations or Streaming respectively. It will not wait for response to be finished, it will return as soon as first chunk of HTTP response body is received.

2.5.9 Reading body as string

http.async.client/string returns either string or seq of strings, again depending if you used Asynchronous operations or Streaming respectively. It will not wait for response to be finished, it will return as soon as first chunk of HTTP response body is received.

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url)]
    (http/string (http/await resp))))

Sample above will return string of response body. http.async.client/string is lazy so you can use it in case of streams as well.

(with-open [client (http/create-client)] ; Create client
  (let [resp    (http/stream-seq client :get url)
        strings (http/string resp)]
    (doseq [part strings]
      (println part))))

Sample above will print parts as they are received, and will return once response receiving is finished.

2.5.10 Reading error

http.async.client/error will return Throwable that was cause of request failure iff request failed, else nil.

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url)]
    (http/await resp)
    (when-let [err (http/error resp)]
      (println "failed processing request: " err))))

2.5.11 Canceling request

At any given time of processing HTTP Response you can cancel it by calling http.async.client/cancel.

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url)]
    (http/cancel resp)))

Please see canceling-request test.

2.5.12 Response predicates

You can also check status of request.

2.5.12.1 done?

http.async.client/done? will tell you if response processing has finished:

(with-open [client (http/create-client)] ; Create client
  (let [resp (http/GET client url)]
    (when-not (http/done? resp)
      (http/await resp)
      (http/done? resp))))

Sample above will check if response was finished, if not - will wait for it and return true as a result of call to done?.

2.5.12.2 failed?

http.async.client/failed? will return true iff request has failed. If this return true you can read error.

2.5.12.3 canceled?

http.async.client/canceled? will return true iff request has been canceled, else false is return.

2.5.13 Requested URL

If you need to get URL of response at hand, there are two associations in it that you will find useful:

:url
encoded url,
:raw-url
not encoded url.

And two convenience functions:

2.6 Managing client

2.6.1 Branding

http.async.client can be configured with User-Agent. To do so you can use http.async.client/create-client and remember to close created client yourself, best is to use it within macro like with-open, though make sure that body of it will wait for whole response to finish.

(with-open [client (http/create-client :user-agent "Your User Agent/1.0")]
  (let [resp (http/GET client url)]
    ;; do stuff with resp
    ))

2.6.2 Enabling HTTP compression

http.async.client can be configured to allow, or not, HTTP compression.

(with-open [client (http/create-client :compression-enabled true)]
  (let [resp (http/GET client url)]
    ;; do stuff with resp
    ))

2.6.3 Follow redirects

Enabling HTTP redirects following.

(with-open [client  (http/create-client :follow-redirects true)]
  (let [resp (http/GET client url)]
    ;; do stuff with resp
    ))

2.6.4 Remove parameters on redirect

Query parameters are not passed on to any redirect that is followed (by default parameters are removed).

(with-open [client (http/create-client :remove-params-on-redirect true)]
  (let [resp (http/GET client url)]
    ;; do stuff with resp
    ))

2.6.5 Keep alive

Keep Alive is enabled by default. This implies using pool for connections.

(with-open [client (http/create-client :keep-alive true)]
  (let [resp (http/GET client url)]
    ;; do stuff with resp
    ))

2.6.6 Max connections per host

Maximum number of connections to be cached per host. Above this number connections will still be created but will not be kept alive.

(with-open [client (http/create-client :max-conns-per-host 10)]
  (let [resp (http/GET client url)]
    ;; do stuff with resp
    ))

2.6.7 Max connections total count

Maximum number of total connections opened, submitting new request while all allowed connections are active, will result in rejection.

(with-open [client (http/create-client :max-conns-total 100)]
  (let [resp (http/GET client url)]
    ;; do stuff with resp
    ))

2.6.8 Max redirects to follow

Maximum number of redirects to follow.

(with-open [client (http/create-client :max-redirects 3)]
  (let [resp (http/GET client url)]
    ;; do stuff with resp
    ))

2.6.9 Timeouts

With http.async.client apart from per connection :timeout you can globally configure connection, request and idle timeouts. All timeout values are in milliseconds and magic value -1 is interpreted as infinite wait. idle connection in pool timeout works only on connections in pool, connections idle, for configured time, in pool will be closed.

(with-open [client (http/create-client :connection-timeout 10
                                       :request-timeout 1000
                                       :idle-in-pool-timeout 100)]
  (let [resp (http/GET client url)]
    ;; request processing
    ))

Example above will timeout connection if it was not established in 10ms, request if it was not received in 1sec, or connection when it was idling in pool for more than 100ms.

2.6.10 Proxy

Client can be also configured with global HTTP Proxy settings.

(with-open [client (http/create-client :proxy {:host h :port p})]
  (let [resp (http/GET client url)]
    ;; do stuff with resp
    ))

Proxy expects a map with following keys:

:host
proxy host
:port
proxy port
:protocol
optional protocol to communicate with proxy. Can be :http (default) or :https.
:user
optional user name to use for proxy authentication. Must be provided with :password.
:password
optional password to use for proxy authentication. Must be provided with :user.

2.6.11 Authentication

Default authentication realm to be used globally can be configured. For now BASIC and DIGEST methods are supported.

Basic method is default, so you don't have to specify it:

(with-open [client (http/create-client :auth {:user u :password p})] 
  (let [resp (http/GET client url)]
    ;; Check if response is not 401 or so and process response
    ))

Though you can:

(with-open [client (http/create-client :auth {:type :basic :user u :password p})]
  (let [resp (http/GET client url)]
    ;; Check if response is not 401 or so and process response
    ))

And for digest method you will need realm as well:

(with-open [client (http/create-client :auth {:type :digest :user u :password p :realm r})]
  (let [resp (http/GET client url)]
    ;; Check if response is not 401 or so and process response
    ))

Preemptive authentication can be enabled or disabled globally per client:

(with-open [client (http/create-client :auth {:type :basic :user u :passowrd p :preemptive true})]
  (let [resp (http/GET client url)]
    ;; process response
    ))

2.6.12 SSL Certificates

Since v0.4.2 it is possible to use SSL certificates with client.

For client to use SSL you need to provide it with ssl-context.

(require [http.async.client.cert :as cert])
(let [ctx (cert/ssl-context :keystore-file ks-file
                            :keystore-password password
                            :certificate-file cert-file
                            :certificate-alias other-cert-alias)]
  (with-open [client (http/create-client :ssl-context ctx)]
    (let [resp (http/GET client url)]
      ;; process response
      )))

For more documentation please consult docstring of http.async.client.cert/ssl-context and tests.

2.6.13 Closing http.async.client

Whenever you've created http.async.client via http.async.client/create-client you will need to close it. To do so you call http.async.client/close.

(let [client (http/create-client)]
  (try
    (let [resp (http/GET client url)]
      ;; process response
      )
    (finally
     (http/close client))))

3 Low level

This is lowest level access to http.async.client. Mechanics here is based on asynchronous call-backs. It provides default set of callbacks and functions to create and execute requests.

3.1 Preparing request

http.async.client.request/prepare-request is responsible for request preparation, like the name suggests. It takes following arguments:

  • HTTP Method like :get :head
  • url that you want to call
  • and options, a keyworded map described already in Request options. Sample:
    (with-open [client (http/create-client)]
      (let [req (request/prepare-request 
                                         :get "http://google.com"
                                         :headers {:my-header "value"})]
        ;; now you have request, next thing to do would be to execute it
        ))
    

3.2 Executing request

http.async.client.request/execute-request returns same map of promises as Asynchronous operations. Its arguments are: request to be executed (result of Preparing request) and options as keyworded map consisting of call-backs. Following options keys are recognized:

:status
Status line
:headers
Headers
:part
Body part
:completed
Body completed
:error
Error

All callbacks take response map as first argument and callback specific argument if any. Callbacks are expected to return tuple of result and action:

result
will be delivered to respective promise in response map
action
if its value is :abort than response processing will be aborted, anything else here will result in continuation.

3.2.1 Status line

Status line callback gets called after status line has been received with arguments:

  • response map
  • Status map has following keys:
    • :code status code (200, 404, ..)
    • :msg status message ("OK", ..)
    • :protocol protocol with version ("HTTP/1.1")
    • :major major protocol version (1)
    • :minor minor protocol version (0, 1)

Sample code to illustrate how to use status callback:

(with-open [client (http/create-client)] ; create client
  (let [request (request/prepare-request :get "http://example.com") ; create request
        status (promise)                ; status promise that will be delivered by callback
        response (request/execute-request
                  client request        ; execute *request*
                  :status               ; status callback
                  (fn [res st]          ; *res* is response map, same as one returned by *execute-request*
                                        ; *st* is status map, as described above
                    (deliver status st) ; deliver status promise
                    [st :abort]))]      ; return status to be delivered to response map and abort further processing of response.
    (println @status)))                 ; await status to be delivered and print it.

3.2.2 Headers

Headers callback gets called after headers have been received with arguments:

  • response map
  • lazy map of headers. Keys in that map are (keyword (.toLowerCase <header name>)), so "Server" headers is :server and so on.

Sample code to illustrate how to use headers callback:

(with-open [client (http/create-client)] ; create client
  (let [request (request/prepare-request :get "http://example.com") ; create request
        headers (promise)               ; headers promise that will be delivered by callback
        response (request/execute-request
                  client request        ; execute *request*
                  :header               ; header callback
                  (fn [res hds]         ; *res* is response map, same as one returned by *execute-request*
                                        ; *hds* is headers map, as described above
                    (deliver headers st) ; deliver headers promise
                    [hds :abort]))]     ; return headers to be delivered to response map and abort further processing of response.
    (println @headers)))

3.2.3 Body part

Body part callback gets called after each part of body has been received with arguments:

  • response map
  • ByteArrayOutputStream that contains body part received.

Following code sample will show how to count number of body parts received:

(with-open [client (http/create-client)] ; create client
  (let [cnt (atom 0)                    ; body parts counter
        request (request/prepare-request :get "http://localhost:8123/stream") ; create request
        resp (execute-request
client* request          ; execute *request*
              :part                     ; body part callback
              (fn [_ p]                 ; ignore response map,
                                        ; *p* is body part
                (swap! cnt inc)         ; increment body part counter
                [p :continue]))]        ; return part to be delivered to response map and continue processing of response.
    (http/await resp)                   ; wait for response to finish
    (println @cnt)))                    ; print body parts counter

Next sample will collect body parts:

(with-open [client (http/create-client)] ; create client
  (let [parts (atom #{})                ; body parts collector
        request (request/prepare-request :get "http://localhost:8123/stream") ; create request
        resp (execute-request
client* request          ; execute *request*
              :part                     ; body part callback
              (fn [_ ^ByteArrayOutputStream p] ; ignore response map,
                                        ; *p* is body part
                (let [p (.toString part "UTF-8")] ; extract text
                  (swap! parts conj p)  ; collect body part
                  [p :continue])))]     ; return part to be delivered to response map and continue processing of response.
    (http/await resp)                   ; wait for response to finish
    (println @parts)))                  ; print collected body parts

3.2.4 Body completed

This callback gets called when receiving of response body has finished with only one argument, i.e. response map.

Following snippet shows how to measure execution time:

(with-open [client (http/create-client)] ; create client
  (let [request (request/prepare-request :get "http://localhost:8123/") ; create request
        finished (promise)               ; execution time will be stored here
        start (System/currentTimeMillis) ; start time
        resp (execute-request
client* request           ; execute *request*
              :completed                 ; completed callback
              (fn [_]                    ; ignore response map,
                (deliver finished        ; deliver execution time
                         (- (System/currentTimeMillis) start))))]
    (println @finished)))                ; print execution time

You have to be aware of fact that :completed callback is called only on successful response. Next snippet will show that :completed callback is not called when request errors:

(with-open [client (http/create-client)] ; create client
  (let [request (request/prepare-request :get "http://not-existing-host/") ; create request
        finished (promise)               ; would get delivered if :completed callback would get executed
        resp (execute-request
client* request           ; execute *request*
              :completed                 ; completed callback, will not fire in this example
              (fn [_]                    ; ignore response map,
                (deliver finished true)))] ; deliver finished promise
    (http/await resp)                    ; wait for response
    (false? (realized? finished))))      ; finished promise never got delivered

3.2.5 Error

Error callback gets called when error while processing has been encountered with arguments

  • response map
  • Throwable that was a cause of failure

Next code snippet shows error callback in use:

(with-open [client (http/create-client)] ; create client
  (let [request (request/prepare-request :get "http://not-existing-host/") ; create request
        errored (promise)                ; will store exception
        resp (execute-request
client* request           ; execute *request*
              :error                     ; error callback
              (fn [_ e]                  ; ignore response map, *e* is exception
                (deliver errored e)))]   ; deliver errored promise
    (http/await resp)                    ; wait for response
    (println @errored)))                 ; print exception from callback

3.3 Default callbacks

http.async.client.request/*default-callbacks* is a map of default callbacks. This will allow you to easy change only few callbacks and reuse default for the rest.

Please look at source of http.async.client/stream-seq to see how to do it.

Date: 2013-02-11T13:53+0100

Author: Hubert Iwaniuk

Org version 7.9.3e with Emacs version 24

Validate XHTML 1.0