Introduction

A ring-inspired, promise-returning, simple Clojure(Script) HTTP client.

Project Maturity

Since httpurr is a young project there can be some API breakage.

Install

The simplest way to use httpurr in a clojure project, is by including it in the dependency vector on your project.clj file:

[funcool/httpurr "1.1.0"]

User Guide

httpurr.client is the namespace containing the functions to perform HTTP requests.

Requests

Requests are maps with the following keys:

  • :method is a keyword with the HTTP method to use. All valid HTTP methods are supported.

  • :url is the URL of the request

  • :headers is a map from strings to strings with the headers of the request.

  • :body is the body of the request.

  • :query-string is a string with the query part of the URL.

  • :query-params is a map that will be encoded as query string.

send! is a function that, given a request map and optionally a map of options, performs the request and returns a promise that will be resolved if there is a response and rejected on timeout, exceptions, HTTP errors or aborts.

Let’s try to make a GET request using the xhr-based client that ships with httpurr, note that this client will only work on browser environments:

(require '[httpurr.client :as http])
(require '[httpurr.client.xhr :refer [client]])

(http/send! client
            {:method :get
             :url "https://api.github.com/orgs/funcool"})

The options map accepts the following keys:

  • :timeout: A time, specified in miliseconds, after which the promise will be rejected with the :timeout keyword as a value.

  • :with-credentials?: A boolean, defaulting to false, which if true will use credentials such as cookies, authorization headers or TLS client certificates during cross-site requests.

Furthermore, the httpurr.client namespaces exposes get, put, post, patch, delete, head, options and trace methods with identical signatures. They all accept the client as the first argument.

These functions have three arities:

  • Two arguments the first is the client and the second is assumed to be the URL of the request.

(http/get client
	   "https://api.github.com/orgs/funcool")
  • Three arguments: like above and the third is the request map without the :url key.

(http/get client
           "https://api.github.com/orgs/funcool"
	   {:headers
            {"Content-Type" "application/json"}})
  • Four arguments: like above and the fourth argument is an option map passed to send!.

(http/get client
           "https://api.github.com/orgs/funcool"
	   {:headers
            {"Content-Type" "application/json"}}
	   {:timeout 2000})

For convenience, client implementations provide aliases for the HTTP methods and the send! function:

(require '[httpurr.client.xhr :as http])

(http/get "https://api.github.com/orgs/funcool")

(http/send! {:method :get
	     :url "https://api.github.com/orgs/funcool"})

Responses

Responses are maps with the following keys:

  • :status is the response status code.

  • :headers is a map from strings to strings with the headers of the response. The names of the headers are normalized to lowercase.

  • :body is the body of the response.

Status Codes

The httpurr.status namespace contains constants for HTTP codes and predicates for discerning the types of responses. They can help you make decissions about how to translate responses to either resolved or rejected promises.

Discerning response types

HTTP has 5 types of responses and httpurr.status provides predicates for checking wheter a response is of a certain type.

For 1xx status codes the predicate is informational?
(require '[httpurr.status :as s])

(s/informational? {:status s/continue})
;; => true
For 2xx status codes the predicate is success?
(require '[httpurr.status :as s])

(s/success? {:status s/ok})
;; => true
For 3xx status codes the predicate is redirection?
(require '[httpurr.status :as s])

(s/redirection? {:status s/moved-permanently})
;; => true
For 4xx status codes the predicate is client-error?
(require '[httpurr.status :as s])

(s/client-error? {:status s/not-found})
;; => true
For 5xx status codes the predicate is server-error?
(require '[httpurr.status :as s])

(s/server-error? {:status s/internal-server-error})
;; => true

Checking status codes

If you need more granularity you can always check for status codes in your responses and transform the promise accordingly.

Let’s say you’re building an API client and you want to perform GET requests for the URL of an entity that can return:

  • 200 OK status code if everything went well

  • 404 not found if the requested entity wasn’t found

  • 401 unauthorized when we don’t have permission to read the resource

We want to transform the promises by extracting the body of the 200 responses and, if we encounter a 404 or 401, return a keyword denoting the type of error. Let’s give it a go:

(require '[httpurr.status :as s])
(require '[httpurr.client.xhr :as xhr])
(require '[promesa.core :as p])

(defn process-response
  [response]
  (condp = (:status response)
    s/ok           (p/resolved (:body response))
    s/not-found    (p/rejected :not-found)
    s/unauthorized (p/rejected :unauthorized)))

(defn id->url
  [id]
  (str "my.api/entity/" id))

(defn entity [id]
  (p/then (xhr/get (id->url id))
          process-response))

Error handling

The Promesa docs explain all the possible combinators for working with promises. We’ve already used then for processing responses, let’s look at two other useful functions: catch and branch.

If we want to attach an error handler to the promise we can use the catch function. Let’s rewrite our previous entity function for handling the error case. We’ll just log the error to the console, you may want to use a better error handling in your code.

(defn entity
  [id]
  (-> (p/then (xhr/get (id->url id))
              process-response)
      (p/catch (fn [err]
                 (.error js/console err)))))

For cases when we want to attach both a success and error handler to a promise we can use the branch function:

(defn entity [id]
  (p/branch (xhr/get (id->url id))
            process-response
            (fn [err]
              (.error js/console err))))

Available clients

ClojureScript

The following clients are available in ClojureScript:

httpurr.client.xhr

XHR-based client for the browser.

httpurr.client.node

Node.js client.

Clojure

httpurr.client.aleph

Aleph-based client.

Implementing your own client

The functions in httpurr.client are based on abstractions defined as protocols in httpurr.protocols so you can implement our own clients.

The following protocols are defined in httpurr.protocols:

  • Client is the protocol for a HTTP client

  • Request is the protocol for HTTP requests

  • Abort is an optional protocol for abortable HTTP requests

  • Response is the protocol for HTTP responses

Take a look at any of the clients under httpurr.client namespace for reference.

Note that the requests passed to the clients have a escaped URL generated as their :url value, inferred from the :url and :query-string from the original requests before being passed to the protocol’s send! function.

Examples

Encoding/Decoding

Since requests and responses are plain maps, we can write simple encoding/decoding function and modify request and responses appropiately. For example, let’s write a decoder function that converts JSON payloads to ClojureScript data structures:

(require '[httpurr.client.node :as node])
(require '[promesa.core :as p])

(defn decode
  [response]
  (update response :body #(js->clj (js/JSON.parse %))))

(defn get!
  [url]
  (p/then (node/get url) decode))

(p/then (get! "http://httpbin.org/get")
         (fn [response]
           (cljs.pprint/pprint response)))
;; {:status 200,
;;  :body
;;  {"args" {},
;;   "headers" {"Host" "httpbin.org"},
;;   "origin" "188.x.x.x",
;;   "url" "http://httpbin.org/get"},
;;  :headers
;;  {"Server" "nginx",
;;   "Date" "Thu, 12 Nov 2015 17:27:50 GMT",
;;   "Content-Type" "application/json",
;;   "Content-Length" "130",
;;   "Connection" "close",
;;   "Access-Control-Allow-Origin" "*",
;;   "Access-Control-Allow-Credentials" "true"}}

Encoding can be achieved similarly applying the map transforming function to requests before sending them:

(defn encode
  [request]
  (update request :body #(js/JSON.stringify (clj->js %))))

(defn post!
  [url req]
  (p/then (node/post url (encode req)) decode))

(p/then (post! "http://httpbin.org/post" {:body {:foo :bar}})
         (fn [response]
           (cljs.pprint/pprint response)))
;; {:status 200,
;;  :body
;;  {"args" {},
;;   "data" "{\"foo\":\"bar\"}",
;;   "files" {},
;;   "form" {},
;;   "headers" {"Content-Length" "13", "Host" "httpbin.org"},
;;   "json" {"foo" "bar"},
;;   "origin" "188.x.x.x",
;;   "url" "http://httpbin.org/post"},
;;  :headers
;;  {"Server" "nginx",
;;   "Date" "Thu, 12 Nov 2015 17:33:59 GMT",
;;   "Content-Type" "application/json",
;;   "Content-Length" "258",
;;   "Connection" "close",
;;   "Access-Control-Allow-Origin" "*",
;;   "Access-Control-Allow-Credentials" "true"}}

Auth

All that is needed for basic is to encode your user and password and add it to your headers along with a WWW-Authenticate header to state your realm here is an example:

(require '[httpurr.client.node :as node])
(require '[promesa.core :as p])
(require '[goog.crypt.base64 :as base64])

(defn auth-header
  [user password]
  (str "Basic " (base64/encodeString (str user ":" password))))

(defn basic
  [realm user password]
  (fn [req]
    (update req
            :headers
            (partial merge {"WWW-Authenticate" (str  "Basic realm=\"" realm "\"")
                      "Authorization" (auth-header user password)}))))


(def credentials (basic "Fake Realm" "Ada" "iinventedprogramming"))

(defn get!
  ([url]
   (get! url {}))
  ([url request]
   (node/get url (credentials request))))

(p/then (get! "http://httpbin.org/basic-auth/Ada/iinventedprogramming")
        (fn [response]
          (cljs.pprint/pprint response)))
;; {:status 200, :body #object[Buffer {
;;   "authenticated": true,
;;   "user": "Ada"
;; }
;; ],
;;  :headers
;;  {"Server" "nginx",
;;   "Date" "Thu, 12 Nov 2015 18:15:51 GMT",
;;   "Content-Type" "application/json",
;;   "Content-Length" "46",
;;   "Connection" "close",
;;   "Access-Control-Allow-Origin" "*",
;;   "Access-Control-Allow-Credentials" "true"}}

A similar approach can be followed for implementing other authentication schemes.

Sending form data

Browser

For sending form data you need to send the FormData instance as the body of the request. Let’s send a form to the httbin.org site and confirm that the form is sent correctly.

(require '[httpurr.client.xhr :as xhr])

(def fd (js/FormData.))
(.append fd "foo" "bar")
(.append fd "baz" "foo")

(defn parse-json-body
  [{:keys [body]}]
  (js/JSON.parse body))

(defn clj-body
  [response]
  (js->clj (parse-json-body response)))

(def req
  (http/post "http://httbin.org/post" {:body fd}))

(p/then req
        (fn [response]
          (let [body (clj-body response)]
            (println :form (get body "form"))
            (println :content-type (get-in body ["headers" "Content-Type"])))))
;; :form {baz foo, foo bar}
;; :content-type multipart/form-data; boundary=----WebKitFormBoundaryg4VACYY9tWU91kvn

FAQ

Why another library?

There are plenty of HTTP client libraries available, each with its own design decisions. Here are the ones made for httpurr.

  • Promises are a natural fit for the request-response nature of HTTP. They contain either an eventual value (the response) or an error value. CSP channels lack first class errors and callbacks/errbacks are cumbersome to compose. httpurr uses promesa to provide a cross-platform promise type and API.

  • A data based API, requests and responses are just maps. This makes easy to create and transform requests piping various transformations together and the same is true for responses.

  • No automatic encoding/decoding based on content type, it sits at a lower level. Is your responsibility to encode and decode data, httpurr just speaks HTTP.

  • Constants with every HTTP status code, sets of status codes and predicates for discerning response types.

  • Pluggable client implementation. Currently httpurr ships with an XHR-based client for the browser, a node client, and a aleph client for Clojure.

  • Intended as a infrastructure lib that sits at the bottom of your HTTP client API, we’ll add things judiciously.

Alternatives?

There are several alternatives, httpurr tries to steal the best of each of them while having a promise-based API which no one offers.

  • cljs-http: Pretty popular and complete, uses CSP channels for responses. Implicitly encodes and decodes data. It has some features like helpers for JSONP and auth that I may eventually add to httpurr.

  • cljs-ajax: Works in both Clojure and ClojureScript. Implicitly encodes and decodes data. Callback-based API.

  • happy: Encoding/decoding are explicit. Callback-based API. Works in both Clojure and ClojureScript. Pluggable clients through global state mutation.

All listed alternatives are licensed with EPL.

Developers Guide

Contributing

Unlike Clojure and other Clojure contrib libs, does not have many restrictions for contributions. Just open a issue or pull request.

Get the Code

httpurr is open source and can be found on github.

You can clone the public repository with this command:

git clone https://github.com/funcool/httpurr

Run tests

To run the tests execute the following:

./scripts/build
node out/tests.js

You will need to have nodejs installed on your system.

License

httpurr is public domain.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

For more information, please refer to <http://unlicense.org/>