Skip to content
Permalink
Browse files

Add ability to specify arbitrary functions to modify http client buil…

…ders (#434)

This adds the `:http-builder-fns` and `:async-http-builder-fns` options to
specify an arbitrary sequence of functions that can be used to modify the
`HttpClientBuilder` and `HttpAsyncClientBuilder` objects before the request is
sent.

Relates to #390
  • Loading branch information
dakrone committed Feb 13, 2018
1 parent 47a7762 commit 2ab22cb75841c712997dc57bfc3e393cdbcd41d1
Showing with 80 additions and 8 deletions.
  1. +28 −0 README.org
  2. +2 −0 changelog.org
  3. +3 −3 src/clj_http/client.clj
  4. +10 −4 src/clj_http/core.clj
  5. +37 −1 test/clj_http/test/core_test.clj
@@ -1062,6 +1062,34 @@ which is a vector of the default middleware that clj-http uses.
=clj-http.client/*current-middleware*= is bound to the current list of
middleware during request time.

** Modifying Apache-specific features of the =HttpClientBuilder= and =HttpAsyncClientBuilder=
:PROPERTIES:
:CUSTOM_ID: h:844f078c-531e-445e-b7ce-76092bcc9928
:END:

While clj-http tries to provide the features needed, there are times when it does not provide access
to a parameter that you need. In these cases, you can use a couple of advanced parameters to provide
arbitrary configuration functions to be run on the =HttpClientBuilder= by specifying
=:http-builder-fns= and =:async-http-builder-fns=.

Each of these variables is a sequence of functions of two arguments, the http builder
(=HttpClientBuilder= for =:http-builder-fns= and =HttpAsyncClientBuilder= for
=:async-http-builder-fns=) and the request map.

#+BEGIN_SRC clojure
;; A function that takes a builder and disables Apache's cookie management
(defun my-cookie-disabler [^HttpClientBuilder builder
request]
(when (:disable-cookies request)
(.disableCookieManagement builder)))

;; The functions to modify the builder are passed in
(http/post "http://www.example.org" {:http-builder-fns [my-cookie-disabler]
:disable-cookies true})
#+END_SRC

The functions are run in the order they are passed in (inside a =doseq=).

* Development
:PROPERTIES:
:CUSTOM_ID: h-65bbf017-2e8b-4c43-824b-24b89cc27a70
@@ -24,6 +24,8 @@ List of user-visible changes that have gone into each release
- Added =:ignore-nested-query-string=, =:flatten-nested-form-params=, and =:flatten-nested-keys=
options for finer-grained control over which nested parts of the request are flattened. Fixes
https://github.com/dakrone/clj-http/issues/427
- Added =:http-builder-fns= and =:async-http-builder-fns= to support arbitrary customizations to the
=HttpClientBuilder= and =HttpAsyncClientBuilder=

** 3.8.0
- Reintroduce the =:save-request= and =:debug-body= options
@@ -256,7 +256,7 @@

(defn- respond*
[resp req]
(if (:async? req)
(if (opt req :async)
((:respond req) resp)
resp))

@@ -1096,8 +1096,8 @@
(throw (IllegalArgumentException. "Host URL cannot be nil"))))

(defn- request*
[{:keys [async?] :as req} [respond raise]]
(if async?
[req [respond raise]]
(if (opt req :async)
(if (some nil? [respond raise])
(throw (IllegalArgumentException.
"If :async? is true, you must pass respond and raise"))
@@ -156,7 +156,8 @@

(defn http-client [{:keys [redirect-strategy retry-handler uri
request-interceptor response-interceptor
proxy-host proxy-port]}
proxy-host proxy-port http-builder-fns]
:as req}
conn-mgr http-url proxy-ignore-host]
;; have to let first, otherwise we get a reflection warning on (.build)
(let [^HttpClientBuilder builder (-> (HttpClients/custom)
@@ -183,11 +184,14 @@
(process [resp ctx]
(response-interceptor
resp ctx)))))
(doseq [http-builder-fn http-builder-fns]
(http-builder-fn builder req))
(.build builder)))

(defn http-async-client [{:keys [redirect-strategy uri
request-interceptor response-interceptor
proxy-host proxy-port] :as req}
proxy-host proxy-port async-http-builder-fns]
:as req}
conn-mgr http-url proxy-ignore-host]
;; have to let first, otherwise we get a reflection warning on (.build)
(let [^HttpAsyncClientBuilder builder (-> (HttpAsyncClients/custom)
@@ -217,6 +221,8 @@
(process [resp ctx]
(response-interceptor
resp ctx)))))
(doseq [async-http-builder-fn async-http-builder-fns]
(async-http-builder-fn builder req))
(.build builder)))

(defn http-get []
@@ -379,11 +385,11 @@
cookie-store cookie-policy headers multipart mime-subtype
http-multipart-mode query-string redirect-strategy max-redirects
retry-handler request-method scheme server-name server-port
socket-timeout uri response-interceptor proxy-host proxy-port async?
socket-timeout uri response-interceptor proxy-host proxy-port
http-client-context http-request-config
proxy-ignore-hosts proxy-user proxy-pass digest-auth ntlm-auth]
:as req} respond raise]
(let [req (dissoc req :async?)
(let [async? (opt req :async)
scheme (name scheme)
http-url (str scheme "://" server-name
(when server-port (str ":" server-port))
@@ -110,10 +110,19 @@
[:get "/query-string"]
{:status 200 :body (:query-string req)}))

(defn add-headers-if-requested [client]
(fn [req]
(let [resp (client req)
add-all (-> req :headers (get "add-headers"))
headers (:headers resp)]
(if add-all
(assoc resp :headers (assoc headers "got" (pr-str (:headers req))))
resp))))

(defn run-server
[]
(defonce server
(ring/run-jetty #'handler {:port 18080 :join? false})))
(ring/run-jetty (add-headers-if-requested handler) {:port 18080 :join? false})))

(defn localhost [path]
(str "http://localhost:18080" path))
@@ -670,3 +679,30 @@
(let [context-for-request (last (last @called-args))]
(is (= http-context context-for-request))
(is (= request-config (.getRequestConfig context-for-request)))))))

(deftest ^:integration test-custom-http-builder-fns
(run-server)
(let [resp (client/get (localhost "/get")
{:headers {"add-headers" "true"}
:http-builder-fns
[(fn [builder req]
(.setDefaultHeaders builder (:hdrs req)))]
:hdrs [(BasicHeader. "foo" "bar")]})]
(is (= 200 (:status resp)))
(is (.contains (get-in resp [:headers "got"]) "\"foo\" \"bar\"")
"Headers should have included the new default headers"))
(let [resp (promise)
error (promise)
f (client/get (localhost "/get")
{:async true
:headers {"add-headers" "true"}
:async-http-builder-fns
[(fn [builder req]
(.setDefaultHeaders builder (:hdrs req)))]
:hdrs [(BasicHeader. "foo" "bar")]}
resp error)]
(.get f)
(is (= 200 (:status @resp)))
(is (.contains (get-in @resp [:headers "got"]) "\"foo\" \"bar\"")
"Headers should have included the new default headers")
(is (not (realized? error)))))

0 comments on commit 2ab22cb

Please sign in to comment.
You can’t perform that action at this time.