From 28efb3fc7a9682d43255743f0c540e53ec7c3c94 Mon Sep 17 00:00:00 2001 From: Raymond Huang Date: Tue, 10 Dec 2019 18:37:57 -0500 Subject: [PATCH] Wrap the response in a future when the property :async-future is set --- README.org | 18 +++++++++++- src/clj_http/client.clj | 28 +++++++++++++++++++ test/clj_http/test/client_test.clj | 45 ++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/README.org b/README.org index 93de7650..a28d8c24 100644 --- a/README.org +++ b/README.org @@ -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]] @@ -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 diff --git a/src/clj_http/client.clj b/src/clj_http/client.clj index 44ec84f7..c02d64ac 100644 --- a/src/clj_http/client.clj +++ b/src/clj_http/client.clj @@ -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) @@ -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))) @@ -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 diff --git a/test/clj_http/test/client_test.clj b/test/clj_http/test/client_test.clj index f284572d..36fbeea6 100644 --- a/test/clj_http/test/client_test.clj +++ b/test/clj_http/test/client_test.clj @@ -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