A Brief Note on How clj-http Works
It’s not Ring, but it’s close
Mark McGranaghan’s Ring library is probably better known than clj-http. It’s largely for serving and responding to HTTP requests. clj-http is the same idea on the opposite side of the protocol. It’s for making HTTP requests.
clj-http’s introduction page is at http://mmcgrana.github.com/2010/08/clj-http-clojure-http-client.html.
Poking at it, trying to make sense of the code, I couldn’t quite grasp what was going on.
Here’s the definition of
clj-http.client/request, featured prominently on clj-http’s introduction page:
(def request (-> #'core/request wrap-redirects wrap-exceptions wrap-decompression wrap-input-coercion wrap-output-coercion wrap-query-params wrap-basic-auth wrap-accept wrap-accept-encoding wrap-content-type wrap-method wrap-url))
request is the building block of higher-level requests that most developers would worry themselves with.
post and their ilk are implemented in terms of
The code flows both ways
As to what exactly request is doing, I think it’s safe to say that all of the things written above… well, happen; redirects are handled, the content is typed, input and output are compressed and/or coerced. But in what order? The
-> operator that I maligned in a previous post turns a series of backwards-lexically function calls into a sequential listing, but in this particular case, that’s misleading as well, as even though
wrap-url is called last, it’s effects on the computation actually occur first. Wha?
It clicked for me when I realized that clj-http is written in a Continuation Passing Style. The only kind of continuation that clj-http worries itself with is the sending off of a request. All of these wrap- methods refer to their continuation as
client, and when they call it, they expect to get back a HTTP response.
Let’s take a look at
(defn wrap-url [client] (fn [req] (if-let [url (:url req)] (client (-> req (dissoc :url) (merge (parse-url url)))) (client req))))
It takes a client, and a client takes a request. A request is a map of various values that affect what gets included in the HTTP request.
wrap-url checks if it’s request has a convenience key on it,
:url, and if so, it breaks the url up into a bunch more specific parts using
parse-url and then merges them with the given request map. Now, the really cool and ingenious part of this is that
wrap-url doesn’t actually do any of this when called, but instead returns a fn that will. That fn — you guessed it — is a “client”, which means that the result of
wrap-url can then be passed to other request-altering fns as their continuation. All of the wrap- methods modify the client you give them to produce one with the underlying client behavior and whatever new behavior they see fit to add.
So, back to the question of when the methods in
request‘s long arrow chain actually take effect: clj-http doesn’t change the semantics of the
-> operator, so
wrap-url does indeed get called last. It is passed the client created by
wrap-method, who was passed the client created by […insert your intelligence from your own explorations here..] which is passed the
clj-http.core/request is the only fn in the whole lot that actually knows how to make an actual http request (and even then it has Apache’s HttpClient do most of the heavy lifting for it).
So the last function in the arrow chain is the first function to get a crack at modifying the request. Conversely, it’s the last function to get a crack at modifying the response as it must get the response from the client passed to it from wrap-method, which must get it’s response from […oh god, not this again…] which gets it from
It is up to each wrap- method along the way to decide whether or not it’s actually concerned with the request or the response (or both). There are plenty of fns in the stack that demonstrate each choice.
wrap-url is a good example of a fn that modifies the request on the way down, but returns the response untouched.
I hope this helps anybody who was thinking about writing stuff on top of clj-http, but couldn’t immediately figure out how to stop their code from automatically redirecting or how to gracefully check that they’ve logged into a website before making requests to pages behind a user account.