janus/http-client

An interceptor-chain-based, async HTTP client library built on top of core.async and Pedestal interceptors. Requests flow through a composable interceptor pipeline before being dispatched by a pluggable backing implementation. The library itself is Ring-compatible and transport-agnostic — it requires a separate implementation package to actually send HTTP requests (e.g. janus/http-client-jetty).

Setup

Add the dependency:

;; deps.edn
{io.forward-publishing.janus/http-client {:mvn/version "..."}}

You also need a backing implementation on the classpath. See janus/http-client-jetty for a Jetty 9–12 implementation.

Using create — low-level client

http/create builds an InterceptorChainClient directly. It accepts keyword arguments and returns a client that satisfies both Stoppable and java.io.Closeable, so it works with with-open.

Option Description

::http/impl

Required. A backing implementation satisfying the HttpClient protocol (e.g. a value returned by jetty/create).

::http/interceptors

Interceptor pipeline. Defaults to interceptors/default-interceptors.

(require '[clojure.core.async :refer [<!!]]
         '[io.forward-publishing.janus.http-client :as http]
         '[io.forward-publishing.janus.http-client.interceptors :as interceptors]
         '[janus.http.client.jetty.api :as jetty])

(with-open [client (http/create ::http/interceptors
                                (conj interceptors/default-interceptors
                                      (interceptors/rate-limiter 1))
                                ::http/impl (jetty/create {}))]
  (<!! (http/submit client "https://example.com")))

Submitting requests

http/submit has two arities:

;; Returns a new channel that will receive the response
(http/submit client request)

;; Puts the response onto the provided channel
(http/submit client request chan)

request can be a URL string (processed by construct-request and explode-uri) or a Ring request map. The response is a Ring response map, or a cognitect anomaly map on failure.

Stopping the client

;; Explicit stop
(http/stop client)

;; Or use with-open — client implements java.io.Closeable
(with-open [client (http/create ::http/impl (jetty/create {}))]
  ...)

Using create-api — named-route API client

http/create-api builds a higher-level client around a set of Reitit routes. Routes must resolve to absolute URLs. The returned value is both a Stoppable and a Clojure function.

Arity Description

(client :op)

Calls the route named :op with no parameters. Returns a channel.

(client :op params)

Calls the route named :op with the given params map. Path params are interpolated; remaining keys become query params. Returns a channel.

(client :op params out-chan)

Same as above, but puts the response onto out-chan.

If ::http/impl is omitted, the client creates and manages its own backing implementation and will stop it when http/stop is called. If you provide ::http/impl externally, you are responsible for stopping it independently.

(require '[clojure.core.async :refer [<!!]]
         '[io.forward-publishing.janus.http-client :as http]
         '[io.forward-publishing.janus.http-client.interceptors :refer [extract-body]]
         '[janus.http.client.jetty.api :as jetty])

(def routes
  ["http://api.example.com"
   {:interceptors [extract-body]}
   ["/users" :users]
   ["/users/{id}" :user]])

(def api (http/create-api routes ::http/impl (jetty/create {})))

;; Call by route name
(<!! (api :users))
(<!! (api :user {:id "42"}))

;; Stop when done (also stops the externally-provided impl if you passed one)
(http/stop api)

Default interceptors

interceptors/default-interceptors is a vector of interceptors applied to every request:

Interceptor Description

construct-request

Converts a bare URI string or java.net.URI into a Ring request map with :uri and :request-method :get.

explode-uri

Parses an absolute URI into Ring keys: :scheme, :server-name, :server-port, :uri, :query-string.

format-query-string

Encodes the :query-params map into a URL-encoded :query-string.

request-body-encoding

Encodes :body according to :body-format / :body-charset and sets the content-type header.

response-body-encoding

Sets the accept header from :accept and decodes the response body using muuntaja.

Additional interceptors available in io.forward-publishing.janus.http-client.interceptors:

Interceptor Description

(rate-limiter ops-per-second)

Limits throughput to at most ops-per-second requests per second using a token-bucket approach.

(with-bearer token)

Injects Authorization: Bearer <token> into every request.

extract-body

On success (2xx), replaces the response with just the decoded body. On failure, throws a cognitect anomaly.

(in-flight-limiter n)

Limits concurrent in-flight requests to at most n using a semaphore channel.