Skip to content

Commit f7cda82

Browse files
committed
response: limit how long a response can take to close
Previously, closing a response would wait on the response data port to get drained, but that meant a buggy (or adversarial) server could block the close perpetually. Likewise, draining a connection during redirect or when #:close? was #t had the same issue. This change reworks closing and draining to have timeouts and to behave as reasonably as they can in the face of such responses, by still draining the data for compliant servers and timing out after one second on close and after the response timeout config on drain.
1 parent 085276b commit f7cda82

File tree

6 files changed

+98
-60
lines changed

6 files changed

+98
-60
lines changed

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

Lines changed: 64 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
racket/lazy-require
88
racket/match
99
racket/port
10+
racket/promise
1011
"common.rkt"
12+
"error.rkt"
1113
"logger.rkt"
1214
"port.rkt")
1315

@@ -32,7 +34,7 @@
3234
response-output
3335
response-history
3436
(contract-out
35-
[make-response (-> bytes? (listof bytes?) input-port? (listof response?) response-closer/c response?)]
37+
[make-response (-> bytes? (listof bytes?) input-port? (listof response?) response-closer/c response-destroyer/c response?)]
3638
[response-body (-> response? bytes?)]
3739
[response-json (-> response? (or/c eof-object? jsexpr?))]
3840
[response-xexpr (-> response? xexpr?)]
@@ -41,12 +43,11 @@
4143
[read-response-json (-> response? (or/c eof-object? jsexpr?))]
4244
[read-response-xexpr (-> response? xexpr?)]
4345
[read-response-xml (-> response? document?)]
44-
[response-drain! (-> response? void?)]
46+
[response-drain! (->* [response?] [(or/c #f (and/c real? (not/c negative?)))] void?)]
4547
[response-close! (-> response? void?)]))
4648

4749
(struct response
48-
(sema
49-
status-line
50+
(status-line
5051
http-version
5152
status-code
5253
status-message
@@ -55,15 +56,19 @@
5556
[data #:mutable]
5657
history
5758
closer
58-
[closed? #:mutable]))
59+
[closed? #:mutable]
60+
destroyer))
5961

6062
(define response-closer/c
6163
(-> response? void?))
6264

65+
(define response-destroyer/c
66+
(-> response? void?))
67+
6368
(define status-code/c
6469
(integer-in 100 999))
6570

66-
(define (make-response status headers output history closer)
71+
(define (make-response status headers output history closer destroyer)
6772
(match status
6873
[(regexp #rx#"^HTTP/(...) ([1-9][0-9][0-9])(?: (.*))?$"
6974
(list status-line
@@ -72,21 +77,21 @@
7277
status-message))
7378
(define-values (retaining-output retain)
7479
(make-retaining-input-port output))
75-
(define the-resp
76-
(response (make-semaphore 1)
77-
status-line
78-
http-version
79-
status-code
80-
(or status-message #"")
81-
headers
82-
retaining-output
83-
#f
84-
history
85-
closer
86-
#f))
87-
(begin0 the-resp
88-
(retain the-resp))]
89-
80+
(define resp
81+
(response
82+
#;status-linse status-line
83+
#;http-version http-version
84+
#;status-code status-code
85+
#;status-message (or status-message #"")
86+
#;headers headers
87+
#;output retaining-output
88+
#;data #f
89+
#;history history
90+
#;close closer
91+
#;closed? #f
92+
#;destroyer destroyer))
93+
(retain resp)
94+
resp]
9095
[_
9196
(raise-argument-error 'status "a valid status line" status)]))
9297

@@ -134,27 +139,46 @@
134139
(define (read-response-xml r)
135140
(read-xml/document (response-output r)))
136141

137-
(define (response-drain! r)
138-
(call-with-semaphore (response-sema r)
139-
(lambda ()
140-
(unless (response-data r)
141-
(define inp (response-output r))
142-
(unless (port-closed? inp)
143-
(define data (port->bytes inp))
144-
(set-response-data! r data)
145-
(close-input-port inp))))))
142+
(define (response-drain! r [t #f])
143+
(unless (response-data r)
144+
(parameterize-break #f
145+
(define drain-promise
146+
(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))))))
154+
(unless (sync/timeout/enable-break t drain-promise)
155+
(raise (make-timeout-error 'drain)))
156+
(force drain-promise))))
146157

147158
(define (response-close! r)
148-
(call-with-semaphore (response-sema r)
149-
(lambda ()
150-
(unless (response-closed? r)
151-
(define inp (response-output r))
152-
(unless (port-closed? inp)
153-
(copy-port inp (open-output-nowhere))
154-
(close-input-port inp))
155-
((response-closer r) r)
156-
(set-response-closed?! r #t)
157-
(log-http-easy-debug "response closed")))))
159+
;; In order to reuse the connection, we need to drain the data port,
160+
;; but draining the data port might block indefinitely, so drain with
161+
;; a timeout and close the connection if the data cannot be drained in
162+
;; time.
163+
(unless (response-closed? r)
164+
(parameterize-break #f
165+
(define drain-promise
166+
(delay/thread
167+
(define in (response-output r))
168+
(unless (port-closed? in)
169+
(copy-port in (open-output-nowhere))
170+
(close-input-port in))))
171+
(define close-thd
172+
(thread
173+
(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))
181+
(force drain-promise))))
158182

159183

160184
;; match expanders ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,9 @@
224224
resp-output
225225
history
226226
(lambda (_)
227-
(session-release sess u conn))))))))
227+
(session-release sess u conn))
228+
(lambda (_)
229+
(http-conn-close! conn))))))))
228230
(with-handlers ([exn:break?
229231
(lambda (e)
230232
(break-thread thd)
@@ -245,12 +247,15 @@
245247
(log-http-easy-warning "request timed out~n method: ~s~n url: ~.s" method urlish)
246248
(raise (make-timeout-error 'request))))))))
247249

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!)
248253
(cond
249254
[(and (positive? redirects-remaining) (redirect? resp))
250255
(define location (bytes->string/utf-8 (response-headers-ref resp 'location)))
251256
(define dest-url (ensure-absolute-url u location))
252257
(log-http-easy-debug "following ~s redirect to ~s" (response-status-code resp) location)
253-
(response-drain! resp)
258+
(response-drain! resp (timeout-config-request timeouts))
254259
(response-close! resp)
255260
(parameterize-break enable-breaks?
256261
(go dest-url
@@ -263,13 +268,12 @@
263268
#:redirects (sub1 redirects-remaining)))]
264269

265270
[(or close? (not stream?))
266-
(begin0 resp
267-
(response-drain! resp)
268-
(response-close! resp))]
271+
(response-drain! resp (timeout-config-request timeouts))
272+
(response-close! resp)
273+
resp]
269274

270275
[else
271-
(begin0 resp
272-
(will-register executor resp response-close!))]))))
276+
resp]))))
273277

274278
(go (->url urlish)))
275279

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
timeout-config-connect
1010
timeout-config-request
1111
(contract-out
12-
[make-timeout-config (->* ()
13-
(#:lease timeout/c
14-
#:connect timeout/c
15-
#:request timeout/c)
16-
timeout-config?)]
12+
[make-timeout-config
13+
(->* []
14+
[#:lease timeout/c
15+
#:connect timeout/c
16+
#:request timeout/c]
17+
timeout-config?)]
1718
[make-request-timeout-evt (-> timeout-config? evt?)]))
1819

1920
(define timeout/c
@@ -22,9 +23,10 @@
2223
(struct timeout-config (lease connect request)
2324
#:transparent)
2425

25-
(define (make-timeout-config #:lease [lease 5]
26-
#:connect [connect 5]
27-
#:request [request 30])
26+
(define (make-timeout-config
27+
#:lease [lease 5]
28+
#:connect [connect 5]
29+
#:request [request 30])
2830
(timeout-config lease connect request))
2931

3032
(define (make-request-timeout-evt t)

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.8.6")
4+
(define version "0.9")
55
(define collection "net")
66
(define deps
77
'(["base" #:version "8.1.0.4"]

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
#:headers [headers null]
1313
#:body [body (open-input-bytes #"")]
1414
#:history [history null]
15-
#:closer [closer void])
16-
(make-response status headers body history closer))
15+
#:closer [closer void]
16+
#:destroyer [destroyer void])
17+
(make-response status headers body history closer destroyer))
1718

1819
(define response-tests
1920
(test-suite

http-easy/http-easy.scrbl

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -568,8 +568,15 @@ scheme and url-encode the path to the socket as the host.
568568
Equivalent to @racket[(read-xml/document (response-output r))].
569569
}
570570

571-
@defproc[(response-drain! [r response?]) void?]{
572-
Drains @racket[r]'s output port.
571+
@defproc[(response-drain! [r response?]
572+
[t (or/c #f (and/c real? (not/c negative?))) #f]) void?]{
573+
Drains @racket[r]'s output port. When @racket[t] is provided, a
574+
timeout error is raised if draining the response takes more than
575+
@racket[t] seconds.
576+
577+
@history[
578+
#:changed "0.9" @elem{Added the @racket[#t] argument.}
579+
]
573580
}
574581

575582
@defproc[(response-close! [r response?]) void?]{

0 commit comments

Comments
 (0)