Skip to content

Commit

Permalink
Wrap the response in a future when the property :async-future is set
Browse files Browse the repository at this point in the history
  • Loading branch information
rymndhng committed Dec 10, 2019
1 parent 4f61aeb commit 28efb3f
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 1 deletion.
18 changes: 17 additions & 1 deletion README.org
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- [[#post][POST]]
- [[#delete][DELETE]]
- [[#async-http-request][Async HTTP Request]]
- [[#async-futures][Async Futures]]
- [[#cancelling-requests][Cancelling Requests]]
- [[#coercions][Coercions]]
- [[#input-coercion][Input coercion]]
Expand Down Expand Up @@ -444,12 +445,27 @@ start an async request is easy, for example:

All exceptions thrown during the request will be passed to the raise callback.

*** Async Futures
Alternatively, if you prefer working with Futures over callbacks for async
requests, there's an option to wrap the response in a Future.

#+begin_src clojure
(def fut (client/get "http://example.com" {:async-future? true}))
@fut
#+end_src

Deref-ing the future will:

1. on success, returns the response map
2. on error, throws an ExecutionException
3. on cancellation, throws a CancellationException

*** Cancelling Requests
:PROPERTIES:
:CUSTOM_ID: cancelling-requests
:END:

Calls to the http methods with =:async true= return an Apache [[https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/concurrent/BasicFuture.html][BasicFuture]] that you can call =.get=
Calls to the http methods with =:async true or :async-future true= return an Apache [[https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/concurrent/BasicFuture.html][BasicFuture]] that you can call =.get=
or =.cancel= on. See the Javadocs for =BasicFuture= [[https://hc.apache.org/httpcomponents-core-ga/httpcore/apidocs/org/apache/http/concurrent/BasicFuture.html][here]]. For instance:

#+BEGIN_SRC clojure
Expand Down
28 changes: 28 additions & 0 deletions src/clj_http/client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
[slingshot.slingshot :refer [throw+]])
(:import (java.io InputStream File ByteArrayOutputStream ByteArrayInputStream EOFException BufferedReader)
(java.net URL UnknownHostException)
(java.util.concurrent Future ExecutionException CancellationException)
(org.apache.http.entity BufferedHttpEntity ByteArrayEntity
InputStreamEntity FileEntity StringEntity)
(org.apache.http.impl.conn PoolingHttpClientConnectionManager)
Expand Down Expand Up @@ -1135,6 +1136,30 @@
(throw (IllegalArgumentException. "If :async? is true, you must pass respond and raise")))
(client req respond raise))

(opt req :async-future)
(let [result (promise)
unwrap (fn [[type response-or-error]]
(case type
:respond response-or-error
:raise (throw (ExecutionException. response-or-error))
:cancelled (throw (CancellationException. "User cancelled request"))))
respond #(deliver result [:respond %])
raise #(deliver result [:raise %])
oncancel #(deliver result [:cancelled])
basic-future (client (-> req
(dissoc :async-future :async-future?)
(assoc :async true
:oncancel oncancel))
respond
raise)]
(reify
Future
(get [_] (unwrap (deref result)))
(get [_ timeout unit] (unwrap (deref result timeout unit)))
(isCancelled [_] (.isCancelled basic-future))
(isDone [_] (.isRealized result))
(cancel [_ interrupt?] (.cancel basic-future interrupt?))))

:else
(client req)))

Expand Down Expand Up @@ -1173,6 +1198,9 @@
* :respond
* :raise
To make an async HTTP request and wrap the result in a future, set the
key :async-future to true.
The following additional behaviors are also automatically enabled:
* Exceptions are thrown for status codes other than 200-207, 300-303, or 307
* Gzip and deflate responses are accepted and decompressed
Expand Down
45 changes: 45 additions & 0 deletions test/clj_http/test/client_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,51 @@
(is (= params (read-fn (:body @resp))))
(is (not (realized? exception)))))))))

(deftest ^:integration roundtrip-async-future
(run-server)
(testing "roundtrip with scheme as keyword"
(let [resp (request {:uri "/get" :method :get
:async-future? true})]
(is (= 200 (:status @resp)))
(is (= "close" (get-in @resp [:headers "connection"])))
(is (= "get" (:body @resp)))))
(testing "roundtrip with scheme as string"
(let [resp (request {:uri "/get" :method :get
:scheme "http"
:async-future? true})]
(is (= 200 (:status @resp)))
(is (= "close" (get-in @resp [:headers "connection"])))
(is (= "get" (:body @resp)))))
(testing "response parsing"
(let [params {:a "1" :b "2"}]
(doseq [[content-type read-fn]
[[nil (comp parse-form-params slurp)]
[:x-www-form-urlencoded (comp parse-form-params slurp)]
[:edn (comp read-string slurp)]
[:transit+json #(client/parse-transit % :json)]
[:transit+msgpack #(client/parse-transit % :msgpack)]]]
(let [resp (request {:uri "/post"
:as :stream
:method :post
:content-type content-type
:flatten-nested-keys []
:form-params params
:async-future? true})]
(is (= 200 (:status @resp)))
(is (= "close" (get-in @resp [:headers "connection"])))
(is (= params (read-fn (:body @resp))))))))
(testing "error handling"
(let [resp (request {:uri "/error" :method :get
:async-future? true})]
(is (thrown? java.util.concurrent.ExecutionException
@resp))))
(testing "can be cancelled"
(let [resp (request {:uri "/timeout" :method :get
:async-future? true})]
(.cancel resp false)
(is (thrown? java.util.concurrent.CancellationException
@resp)))))

(def ^:dynamic *test-dynamic-var* nil)

(deftest ^:integration async-preserves-dynamic-variable-bindings
Expand Down

0 comments on commit 28efb3f

Please sign in to comment.