From b468bdbce0114703a3fa60efdee40b3a10f90c11 Mon Sep 17 00:00:00 2001 From: Raymond Huang Date: Wed, 4 Sep 2019 23:29:23 -0700 Subject: [PATCH 1/2] Add :async-future as an option for async request processing --- README.org | 13 +++++++- src/clj_http/client.clj | 18 +++++++++-- test/clj_http/test/client_test.clj | 51 ++++++++++++++++++++++++++++-- 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/README.org b/README.org index 833fc282..3c7b464e 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]] @@ -429,12 +430,22 @@ 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, you can get the async response in the shape of a Future: + +#+begin_src clojure +(def fut (client/get "http://example.com" {:async-future? true})) +@fut +#+end_src + +Deref-ing the future will return the response map or throw a [[https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutionException.html][ExecutionException]] on error. + *** 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 12c96174..ad8479a6 100644 --- a/src/clj_http/client.clj +++ b/src/clj_http/client.clj @@ -1168,12 +1168,26 @@ (throw (IllegalArgumentException. "Host URL cannot be nil")))) (defn- request* - [req [respond raise]] - (if (opt req :async) + [req & [respond raise]] + {:pre [(not (and (:async req) (:async-future req)))]} + (cond + (opt req :async) (if (some nil? [respond raise]) (throw (IllegalArgumentException. "If :async? is true, you must pass respond and raise")) (request (dissoc req :respond :raise) respond raise)) + + (opt req :async-future) + (let [basic-future (org.apache.http.concurrent.BasicFuture. nil)] + (request (-> req + (dissoc :async-future) + (assoc :async true + :oncancel #(.cancel basic-future))) + #(.completed basic-future %) + #(.failed basic-future %)) + basic-future) + + :default (request req))) (defn get diff --git a/test/clj_http/test/client_test.clj b/test/clj_http/test/client_test.clj index c15d4146..eb590dfc 100644 --- a/test/clj_http/test/client_test.clj +++ b/test/clj_http/test/client_test.clj @@ -25,9 +25,9 @@ (defn request ([req] - (client/request (merge base-req req))) + (#'client/request* (merge base-req req))) ([req respond raise] - (client/request (merge base-req req) respond raise))) + (#'client/request* (merge base-req req) respond raise))) (defn parse-form-params [s] (->> (str/split (form-decode-str s) #"&") @@ -90,6 +90,14 @@ (is (= "get" (:body @resp))) (is (not (realized? exception)))) + ;; roundrtrip with error handling + (let [resp (promise) + exception (promise) + _ (request {:uri "/error" :method :get + :scheme "http" + :async? true} resp exception)] + (is (instance? Exception @exception))) + (let [params {:a "1" :b "2"}] (doseq [[content-type read-fn] [[nil (comp parse-form-params slurp)] @@ -111,6 +119,45 @@ (is (= params (read-fn (:body @resp)))) (is (not (realized? exception))))))) +(deftest ^:integration roundtrip-async-future + (run-server) + ;; roundtrip with scheme as a 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)))) + ;; roundtrip with scheme as a 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)))) + ;; error handling + (let [resp (request {:uri "/error" :method :get + :async-future? true})] + (is (thrown? java.util.concurrent.ExecutionException + @resp))) + + (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)))))))) + (def ^:dynamic *test-dynamic-var* nil) (deftest ^:integration async-preserves-dynamic-variable-bindings From c5b0c996d35b85e1138c3caf3bcffd348df5f0cd Mon Sep 17 00:00:00 2001 From: Raymond Huang Date: Sat, 21 Sep 2019 22:44:28 -0700 Subject: [PATCH 2/2] Address PR feedback --- src/clj_http/client.clj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/clj_http/client.clj b/src/clj_http/client.clj index ad8479a6..908c0ed1 100644 --- a/src/clj_http/client.clj +++ b/src/clj_http/client.clj @@ -1169,7 +1169,7 @@ (defn- request* [req & [respond raise]] - {:pre [(not (and (:async req) (:async-future req)))]} + {:pre [(not (and (opt req :async) (opt req :async-future)))]} (cond (opt req :async) (if (some nil? [respond raise]) @@ -1180,7 +1180,7 @@ (opt req :async-future) (let [basic-future (org.apache.http.concurrent.BasicFuture. nil)] (request (-> req - (dissoc :async-future) + (dissoc :async :async? :async-future :async-future?) (assoc :async true :oncancel #(.cancel basic-future))) #(.completed basic-future %)