Skip to content

Commit b4d0991

Browse files
committed
Don't leak unconsumed deferreds in error state in HTTP client
To that end, turn `wrap-exceptions` from a wrapping middleware into an "after handler" (like `handle-redirects`) called `handle-error-status`. This also happens to improve a suboptimal behavior: When a request with `:throw-exceptions? true` was made and an error response was received which the middleware would turn into an exception, the underlying connection would be closed regardless of whether the response indicated keep-alive or not. Fixes #766 except for cases originating from Manifold's `d/timeout!` which needs to be fixed there.
1 parent a109c08 commit b4d0991

File tree

4 files changed

+95
-36
lines changed

4 files changed

+95
-36
lines changed

src/aleph/http.clj

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -487,14 +487,17 @@
487487
(fn handle-response [rsp]
488488
(->> rsp
489489
(middleware/handle-cookies req)
490-
(middleware/handle-redirects request req))))))))))))
490+
(middleware/handle-redirects request req)
491+
(middleware/handle-error-status req))))))))))))
491492
req))]
492493
(d/connect response result)
493-
(d/catch' result
494-
(fn [e]
495-
(log/trace e "Request failed. Disposing of connection...")
496-
(@dispose-conn!)
497-
(d/error-deferred e)))
494+
(-> result
495+
(d/catch'
496+
(fn [e]
497+
(when-not (:aleph/error-status? (ex-data e))
498+
(log/trace e "Request failed. Disposing of connection...")
499+
(@dispose-conn!)
500+
true))))
498501
result)))
499502

500503
(defn cancel-request!

src/aleph/http/client_middleware.clj

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -254,36 +254,40 @@
254254
(str "application/" (name type))
255255
type))
256256

257-
(defn wrap-exceptions
257+
(defn error-status-deferred [rsp]
258+
(d/error-deferred
259+
(ex-info
260+
(str "status: " (:status rsp))
261+
(assoc rsp :aleph/error-status? true))))
262+
263+
(defn handle-error-status
258264
"Middleware that throws response as an ExceptionInfo if the response has
259265
unsuccessful status code. :throw-exceptions set to false in the request
260266
disables this middleware."
261-
[client]
262-
(fn [req]
263-
(d/let-flow' [{:keys [status body] :as rsp} (client req)]
264-
(if (unexceptional-status? status)
265-
rsp
266-
(cond
267-
268-
(false? (opt req :throw-exceptions))
269-
rsp
270-
271-
(instance? InputStream body)
272-
(d/chain' (d/future (bs/to-byte-array body))
273-
(fn [body]
274-
(d/error-deferred
275-
(ex-info
276-
(str "status: " status)
277-
(assoc rsp :body (ByteArrayInputStream. body))))))
278-
279-
:else
280-
(d/chain'
281-
(s/reduce conj [] body)
282-
(fn [body]
283-
(d/error-deferred
284-
(ex-info
285-
(str "status: " status)
286-
(assoc rsp :body (s/->source body)))))))))))
267+
[req rsp]
268+
(let [{:keys [status body]} rsp]
269+
(cond
270+
(unexceptional-status? status)
271+
rsp
272+
273+
(false? (opt req :throw-exceptions))
274+
rsp
275+
276+
(instance? InputStream body)
277+
(d/chain' (d/future (bs/to-byte-array body))
278+
(fn [body]
279+
(error-status-deferred
280+
(assoc rsp :body (ByteArrayInputStream. body)))))
281+
282+
(nil? body)
283+
(error-status-deferred rsp)
284+
285+
:else
286+
(d/chain'
287+
(s/reduce conj [] body)
288+
(fn [body]
289+
(error-status-deferred
290+
(assoc rsp :body (s/->source body))))))))
287291

288292
(defn wrap-method
289293
"Middleware converting the :method option into the :request-method option"
@@ -965,7 +969,6 @@
965969
by default"
966970
[client]
967971
(let [client' (-> client
968-
wrap-exceptions
969972
wrap-request-timing)]
970973
(fn [req]
971974
(let [executor (ex/executor)]

test/aleph/http/client_middleware_test.clj

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
(:require
33
[aleph.http.client-middleware :as middleware]
44
[aleph.http.schema :as schema]
5+
[clojure.java.io :as io]
56
[clojure.test :as t :refer [deftest is testing]]
6-
[malli.core :as m]
7-
[malli.generator :as mg])
7+
[malli.generator :as mg]
8+
[manifold.stream :as s])
89
(:import
10+
(clojure.lang ExceptionInfo)
911
(java.net URLDecoder)))
1012

1113
(deftest test-empty-query-string
@@ -203,3 +205,42 @@
203205
#"Invalid spec.*:in \[:content-type\].*:value 10"
204206
(middleware/wrap-validation {:request-method :post
205207
:content-type 10}))))
208+
209+
(deftest test-handle-error-status
210+
(let [handle-error-status (fn [throw? rsp]
211+
(deref (middleware/handle-error-status {:throw-exceptions? throw?}
212+
rsp)
213+
1000
214+
::timeout))]
215+
(testing "response body is input stream"
216+
(try
217+
(let [rsp (handle-error-status true
218+
{:status 400
219+
:body (io/input-stream (.getBytes "hello"))})]
220+
(is (= ::thrown? rsp) "should have thrown"))
221+
(catch Exception e
222+
(is (instance? ExceptionInfo e))
223+
(is (= 400 (:status (ex-data e))))
224+
(is (= "hello" (some-> e ex-data :body slurp))))))
225+
(testing "response body is manifold stream"
226+
(try
227+
(let [rsp (handle-error-status true
228+
{:status 400
229+
:body (doto (s/stream 1)
230+
(s/put! "hello")
231+
(s/close!))})]
232+
(is (= ::thrown? rsp) "should have thrown"))
233+
(catch Exception e
234+
(is (instance? ExceptionInfo e))
235+
(is (= 400 (:status (ex-data e))))
236+
(is (= "hello" (some-> e ex-data :body s/take! (deref 100 :timeout)))))))
237+
(testing "response body is nil"
238+
(try
239+
(let [rsp (handle-error-status true
240+
{:status 400
241+
:body nil})]
242+
(is (= ::thrown? rsp) "should have thrown"))
243+
(catch Exception e
244+
(is (instance? ExceptionInfo e))
245+
(is (= 400 (:status (ex-data e))))
246+
(is (nil? (-> e ex-data :body))))))))

test/aleph/http_test.clj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,18 @@
11941194
(d/timeout! 5e3)
11951195
deref)))))
11961196

1197+
(deftest test-client-throw-error-responses-as-exceptions
1198+
(with-http-servers (fn [_]
1199+
{:status 429
1200+
:body "nope"}) {}
1201+
(try
1202+
(-> (http-get "/" {:throw-exceptions? true})
1203+
(d/timeout! 1e3)
1204+
deref)
1205+
(is (= false true) "should have thrown an exception")
1206+
(catch Exception e
1207+
(is (= 429 (:status (ex-data e))))))))
1208+
11971209
(deftest test-client-errors-handling
11981210
(testing "writing invalid request message"
11991211
(with-handler echo-string-handler

0 commit comments

Comments
 (0)