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.
informational?
(require '[httpurr.status :as s])
(s/informational? {:status s/continue})
;; => true
success?
(require '[httpurr.status :as s])
(s/success? {:status s/ok})
;; => true
redirection?
(require '[httpurr.status :as s])
(s/redirection? {:status s/moved-permanently})
;; => true
client-error?
(require '[httpurr.status :as s])
(s/client-error? {:status s/not-found})
;; => true
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/>