Skip to content

Commit be732f1

Browse files
committed
session+response: work harder to ensure errors during drain are handled correctly
1 parent 2aaa274 commit be732f1

File tree

7 files changed

+154
-108
lines changed

7 files changed

+154
-108
lines changed

http-easy-lib/http-easy/private/pool.rkt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
[pool? (-> any/c boolean?)]
4141
[pool-lease (->* [pool?] [(or/c #f timeout-config?)] http-conn?)]
4242
[pool-release (-> pool? http-conn? void?)]
43+
[pool-abandon (-> pool? http-conn? void?)]
4344
[pool-close! (-> pool? void?)]))
4445

4546
(define connector/c
@@ -97,6 +98,9 @@
9798
(define (pool-release p c)
9899
(d:pool-release! (pool-impl p) c))
99100

101+
(define (pool-abandon p c)
102+
(d:pool-abandon! (pool-impl p) c))
103+
100104
(define (pool-close! p)
101105
(d:pool-close! (pool-impl p))
102106
(log-http-easy-debug "connection pool closed"))

http-easy-lib/http-easy/private/response.rkt

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
response-output
3535
response-history
3636
(contract-out
37-
[make-response (-> bytes? (listof bytes?) input-port? (listof response?) response-closer/c response-destroyer/c response?)]
37+
[make-response (-> bytes? (listof bytes?) input-port? (listof response?) response-closer/c response-abandoner/c response?)]
3838
[response-body (-> response? bytes?)]
3939
[response-json (-> response? (or/c eof-object? jsexpr?))]
4040
[response-xexpr (-> response? xexpr?)]
@@ -44,7 +44,8 @@
4444
[read-response-xexpr (-> response? xexpr?)]
4545
[read-response-xml (-> response? document?)]
4646
[response-drain! (->* [response?] [(or/c #f (and/c real? (not/c negative?)))] void?)]
47-
[response-close! (-> response? void?)]))
47+
[response-close! (-> response? void?)]
48+
[response-closed? (-> response? boolean?)]))
4849

4950
(struct response
5051
(status-line
@@ -57,18 +58,18 @@
5758
history
5859
closer
5960
[closed? #:mutable]
60-
destroyer))
61+
abandoner))
6162

6263
(define response-closer/c
6364
(-> response? void?))
6465

65-
(define response-destroyer/c
66+
(define response-abandoner/c
6667
(-> response? void?))
6768

6869
(define status-code/c
6970
(integer-in 100 999))
7071

71-
(define (make-response status headers output history closer destroyer)
72+
(define (make-response status headers output history closer abandoner)
7273
(match status
7374
[(regexp #rx#"^HTTP/(...) ([1-9][0-9][0-9])(?: (.*))?$"
7475
(list status-line
@@ -89,7 +90,7 @@
8990
#;history history
9091
#;close closer
9192
#;closed? #f
92-
#;destroyer destroyer))
93+
#;abandoner abandoner))
9394
(retain resp)
9495
resp]
9596
[_
@@ -142,17 +143,17 @@
142143
(define (response-drain! r [t #f])
143144
(unless (response-data r)
144145
(parameterize-break #f
146+
(log-http-easy-debug "draining response ~.s" r)
145147
(define drain-promise
146148
(delay/thread
147-
(with-handlers ([exn:break? void])
148-
(parameterize-break #t
149-
(define in (response-output r))
150-
(unless (port-closed? in)
151-
(define data (port->bytes in))
152-
(set-response-data! r data)
153-
(close-input-port in))))))
149+
(define in (response-output r))
150+
(unless (port-closed? in)
151+
(define data (port->bytes in))
152+
(set-response-data! r data)
153+
(close-input-port in))))
154154
(unless (sync/timeout/enable-break t drain-promise)
155-
(raise (make-timeout-error 'drain)))
155+
(log-http-easy-debug "timed out while draining response")
156+
(raise (make-timeout-error 'request)))
156157
(force drain-promise))))
157158

158159
(define (response-close! r)
@@ -162,6 +163,8 @@
162163
;; time.
163164
(unless (response-closed? r)
164165
(parameterize-break #f
166+
(log-http-easy-debug "closing response ~.s" r)
167+
(set-response-closed?! r #t)
165168
(define drain-promise
166169
(delay/thread
167170
(define in (response-output r))
@@ -171,13 +174,14 @@
171174
(define close-thd
172175
(thread
173176
(lambda ()
174-
(unless (sync/timeout 1 drain-promise)
175-
(log-http-easy-warning "timed out while closing response")
176-
((response-destroyer r) r))
177-
((response-closer r) r)
178-
(log-http-easy-debug "response closed"))))
179-
(set-response-closed?! r #t)
180-
(sync/enable-break (thread-dead-evt close-thd))
177+
(cond
178+
[(sync/timeout 1 drain-promise)
179+
(log-http-easy-debug "response closed")
180+
((response-closer r) r)]
181+
[else
182+
(log-http-easy-debug "timed out while closing response; abandoning connection")
183+
((response-abandoner r) r)]))))
184+
(sync/enable-break close-thd)
181185
(force drain-promise))))
182186

183187

http-easy-lib/http-easy/private/session.rkt

Lines changed: 70 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,21 @@
120120
(make-pool (session-conf s) connector))))))
121121
(pool-lease p timeouts)))
122122

123-
(define (session-release s url c)
123+
(define (get-session-pool s url)
124124
(define k (pool-key url))
125-
(define p
126-
(call-with-semaphore (session-sema s)
127-
(lambda ()
128-
(hash-ref (session-pools s) k #f))))
129-
(when p
130-
(log-http-easy-debug "releasing connection to pool ~a" k)
131-
(pool-release p c)))
125+
(call-with-semaphore (session-sema s)
126+
(lambda ()
127+
(hash-ref (session-pools s) k #f))))
128+
129+
(define (session-release s url c)
130+
(define p (get-session-pool s url))
131+
(log-http-easy-debug "releasing connection to pool ~s" (eq-hash-code p))
132+
(pool-release p c))
133+
134+
(define (session-abandon s url c)
135+
(define p (get-session-pool s url))
136+
(log-http-easy-debug "abandoning connection of pool ~s" (eq-hash-code p))
137+
(pool-abandon p c))
132138

133139
(define supplied?
134140
(compose1 not unsupplied-arg?))
@@ -176,19 +182,18 @@
176182
(data headers)
177183
(values headers data))])
178184
(parameterize-break #f
179-
(define conn (session-lease sess u timeouts))
185+
(define conn
186+
(session-lease sess u timeouts))
180187
(define resp
181188
(with-handlers ([exn:break?
182189
(lambda (e)
183-
(log-http-easy-warning "received break")
184-
(http-conn-close! conn)
185-
(session-release sess u conn)
190+
(log-http-easy-warning "received break during request processing; abandoning connection")
191+
(session-abandon sess u conn)
186192
(raise e))]
187193
[exn:fail?
188194
(lambda (e)
189195
(log-http-easy-warning "request failed: ~a" (exn-message e))
190-
(http-conn-close! conn)
191-
(session-release sess u conn)
196+
(session-abandon sess u conn)
192197
(cond
193198
[(exn:fail:http-easy? e)
194199
(log-http-easy-warning "error cannot be retried; bubbling up exception")
@@ -202,7 +207,7 @@
202207
(raise e)]))])
203208
(define resp-ch
204209
(make-channel))
205-
(define thd
210+
(define resp-thd
206211
(thread
207212
(lambda ()
208213
(with-handlers ([exn:break? void]
@@ -223,57 +228,67 @@
223228
resp-headers
224229
resp-output
225230
history
226-
(lambda (_)
227-
(session-release sess u conn))
228-
(lambda (_)
229-
(http-conn-close! conn))))))))
231+
(λ (_) (session-release sess u conn))
232+
(λ (_) (session-abandon sess u conn))))))))
230233
(with-handlers ([exn:break?
231234
(lambda (e)
232-
(break-thread thd)
235+
(break-thread resp-thd)
233236
(raise e))])
234237
(sync/enable-break
235238
(handle-evt
236239
resp-ch
237-
(lambda (r)
238-
(when (exn:fail? r)
239-
(raise r))
240-
(begin0 r
241-
(log-http-easy-debug "response: ~.s" (response-status-line r))
242-
(maybe-save-cookies! sess u (response-headers r)))))
240+
(lambda (resp) ;; noqa
241+
(when (exn:fail? resp)
242+
(raise resp))
243+
(log-http-easy-debug "response: ~.s" (response-status-line resp))
244+
(maybe-save-cookies! sess u (response-headers resp))
245+
resp))
243246
(handle-evt
244247
(make-request-timeout-evt timeouts)
245248
(lambda (_)
246-
(break-thread thd)
249+
(break-thread resp-thd)
247250
(log-http-easy-warning "request timed out~n method: ~s~n url: ~.s" method urlish)
248251
(raise (make-timeout-error 'request))))))))
249-
250-
;; Register executor early in case the calls to response-drain!
251-
;; raise an exception. Closing the response twice is a no-op.
252-
(will-register executor resp response-close!)
253-
(cond
254-
[(and (positive? redirects-remaining) (redirect? resp))
255-
(define location (bytes->string/utf-8 (response-headers-ref resp 'location)))
256-
(define dest-url (ensure-absolute-url u location))
257-
(log-http-easy-debug "following ~s redirect to ~s" (response-status-code resp) location)
258-
(response-drain! resp (timeout-config-request timeouts))
259-
(response-close! resp)
260-
(parameterize-break enable-breaks?
261-
(go dest-url
262-
#:method (case (response-status-code resp)
263-
[(301 302 303) 'get]
264-
[(307 308) method])
265-
#:headers (hash-remove headers 'authorization)
266-
#:auth (and (same-origin? dest-url u) auth)
267-
#:history (cons resp history)
268-
#:redirects (sub1 redirects-remaining)))]
269-
270-
[(or close? (not stream?))
271-
(response-drain! resp (timeout-config-request timeouts))
272-
(response-close! resp)
273-
resp]
274-
275-
[else
276-
resp]))))
252+
;; When an error occurs at this point, a response-close! may already be in progress, so
253+
;; check that the response hasn't been closed already before abandoning the connection.
254+
(with-handlers ([exn:break?
255+
(lambda (e)
256+
(log-http-easy-warning "received break during response processing")
257+
(unless (response-closed? resp)
258+
(log-http-easy-warning "abandoning connection")
259+
(session-abandon sess u conn))
260+
(raise e))]
261+
[exn:fail?
262+
(lambda (e)
263+
(log-http-easy-warning "response processing failed")
264+
(unless (response-closed? resp)
265+
(session-abandon sess u conn))
266+
(raise e))])
267+
(cond
268+
[(and (positive? redirects-remaining) (redirect? resp))
269+
(define location (bytes->string/utf-8 (response-headers-ref resp 'location)))
270+
(define dest-url (ensure-absolute-url u location))
271+
(log-http-easy-debug "following ~s redirect to ~s" (response-status-code resp) location)
272+
(response-drain! resp (timeout-config-request timeouts))
273+
(response-close! resp)
274+
(parameterize-break enable-breaks?
275+
(go dest-url
276+
#:method (case (response-status-code resp)
277+
[(301 302 303) 'get]
278+
[(307 308) method])
279+
#:headers (hash-remove headers 'authorization)
280+
#:auth (and (same-origin? dest-url u) auth)
281+
#:history (cons resp history)
282+
#:redirects (sub1 redirects-remaining)))]
283+
[(or close? (not stream?))
284+
(response-drain! resp (timeout-config-request timeouts))
285+
(response-close! resp)
286+
resp]
287+
[else
288+
;; No more exceptions to handle at this point, so it is safe to register a will
289+
;; executor for the response and return it.
290+
(will-register executor resp response-close!)
291+
resp])))))
277292

278293
(go (->url urlish)))
279294

http-easy-lib/info.rkt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#lang info
22

33
(define license 'BSD-3-Clause)
4-
(define version "0.9")
4+
(define version "0.9.1")
55
(define collection "net")
66
(define deps
77
'(["base" #:version "8.1.0.4"]

http-easy-test/net/http-easy/http-easy.rkt

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,27 @@
419419
(parameterize ([current-session (make-session)])
420420
(check-equal? (response-body (get (format "http://127.0.0.1:~a" port))) #"ok")
421421
;; https://github.com/Bogdanp/racket-http-easy/issues/25
422-
(check-equal? (response-body (get (string->url/literal (format "http://127.0.0.1:~a/a%2Bb.mp3" port)))) #"ok"))))))
422+
(check-equal? (response-body (get (string->url/literal (format "http://127.0.0.1:~a/a%2Bb.mp3" port)))) #"ok")))))
423+
424+
(test-case "response-close! on a server that won't respond in time"
425+
(define sema (make-semaphore))
426+
(call-with-tcp-server
427+
(lambda (_lines out)
428+
(fprintf out "HTTP/1.1 200 OK\r\n")
429+
(fprintf out "Content-Length: 1000\r\n")
430+
(fprintf out "\r\n")
431+
(flush-output out)
432+
(semaphore-wait sema))
433+
(lambda (port)
434+
(parameterize ([current-session (make-session)])
435+
(define resp (get #:stream? #t (format "http://127.0.0.1:~a" port)))
436+
(check-exn
437+
#rx"input port is closed"
438+
(lambda ()
439+
(response-close! resp)))
440+
(semaphore-post sema)
441+
(sync (system-idle-evt))
442+
(session-close! (current-session)))))))
423443

424444
(test-suite
425445
"custom port"
@@ -510,28 +530,33 @@
510530

511531
(test-case "breaking is safe"
512532
(define sema (make-semaphore))
513-
(call-with-web-server
514-
(lambda (_req)
515-
(response/output
516-
(lambda (out)
517-
(semaphore-wait sema)
518-
(displayln "hello, world!" out))))
519-
(lambda (addr)
520-
(parameterize ([current-session
521-
(make-session
522-
#:pool-config
523-
(make-pool-config
524-
#:max-size 1))])
525-
(define thd
526-
(thread
527-
(lambda ()
528-
(with-handlers ([exn:break? void])
529-
(get addr)))))
530-
(sync (system-idle-evt))
531-
(break-thread thd)
532-
(semaphore-post sema)
533-
(semaphore-post sema)
534-
(check-not-false (get addr))))))))))
533+
(for ([_ (in-range 10)])
534+
(call-with-web-server
535+
(lambda (_req)
536+
(response/output
537+
(lambda (out)
538+
(semaphore-wait sema)
539+
(displayln "hello, world!" out))))
540+
(lambda (addr)
541+
(parameterize ([current-session
542+
(make-session
543+
#:pool-config
544+
(make-pool-config
545+
#:max-size 1))])
546+
(define broken?-sema
547+
(make-semaphore))
548+
(define thd
549+
(thread
550+
(lambda ()
551+
(with-handlers ([exn:break? void])
552+
(get addr))
553+
(semaphore-post broken?-sema))))
554+
(sync (system-idle-evt))
555+
(break-thread thd)
556+
(semaphore-post sema)
557+
(semaphore-post sema)
558+
(semaphore-wait broken?-sema)
559+
(check-not-false (get addr)))))))))))
535560

536561
(module+ test
537562
(require rackunit/text-ui)

0 commit comments

Comments
 (0)