httpx 1.1.5 → 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -6
  3. data/doc/release_notes/1_1_1.md +2 -2
  4. data/doc/release_notes/1_2_0.md +49 -0
  5. data/doc/release_notes/1_2_1.md +6 -0
  6. data/lib/httpx/adapters/webmock.rb +25 -3
  7. data/lib/httpx/altsvc.rb +57 -2
  8. data/lib/httpx/buffer.rb +8 -0
  9. data/lib/httpx/chainable.rb +48 -29
  10. data/lib/httpx/connection/http1.rb +27 -22
  11. data/lib/httpx/connection/http2.rb +7 -3
  12. data/lib/httpx/connection.rb +52 -62
  13. data/lib/httpx/extensions.rb +0 -15
  14. data/lib/httpx/options.rb +85 -28
  15. data/lib/httpx/plugins/aws_sigv4.rb +2 -2
  16. data/lib/httpx/plugins/basic_auth.rb +1 -1
  17. data/lib/httpx/plugins/callbacks.rb +91 -0
  18. data/lib/httpx/plugins/circuit_breaker.rb +2 -0
  19. data/lib/httpx/plugins/cookies.rb +19 -9
  20. data/lib/httpx/plugins/digest_auth.rb +1 -1
  21. data/lib/httpx/plugins/follow_redirects.rb +11 -0
  22. data/lib/httpx/plugins/grpc.rb +2 -2
  23. data/lib/httpx/plugins/h2c.rb +20 -8
  24. data/lib/httpx/plugins/proxy/socks4.rb +2 -2
  25. data/lib/httpx/plugins/proxy/socks5.rb +2 -2
  26. data/lib/httpx/plugins/proxy.rb +16 -34
  27. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  28. data/lib/httpx/plugins/retries.rb +4 -0
  29. data/lib/httpx/plugins/ssrf_filter.rb +142 -0
  30. data/lib/httpx/plugins/stream.rb +1 -1
  31. data/lib/httpx/plugins/upgrade/h2.rb +1 -1
  32. data/lib/httpx/plugins/upgrade.rb +1 -1
  33. data/lib/httpx/plugins/webdav.rb +1 -1
  34. data/lib/httpx/pool.rb +32 -28
  35. data/lib/httpx/request/body.rb +3 -3
  36. data/lib/httpx/request.rb +3 -5
  37. data/lib/httpx/resolver/https.rb +10 -4
  38. data/lib/httpx/resolver/native.rb +1 -0
  39. data/lib/httpx/resolver/resolver.rb +17 -6
  40. data/lib/httpx/response/body.rb +3 -0
  41. data/lib/httpx/response.rb +3 -2
  42. data/lib/httpx/session.rb +13 -82
  43. data/lib/httpx/timers.rb +3 -10
  44. data/lib/httpx/transcoder.rb +1 -1
  45. data/lib/httpx/version.rb +1 -1
  46. data/sig/altsvc.rbs +33 -0
  47. data/sig/chainable.rbs +1 -0
  48. data/sig/connection/http1.rbs +2 -1
  49. data/sig/connection.rbs +16 -16
  50. data/sig/options.rbs +10 -2
  51. data/sig/plugins/callbacks.rbs +38 -0
  52. data/sig/plugins/cookies.rbs +2 -0
  53. data/sig/plugins/follow_redirects.rbs +2 -0
  54. data/sig/plugins/proxy/socks4.rbs +2 -1
  55. data/sig/plugins/proxy/socks5.rbs +2 -1
  56. data/sig/plugins/proxy.rbs +11 -1
  57. data/sig/pool.rbs +1 -3
  58. data/sig/resolver/resolver.rbs +3 -1
  59. data/sig/session.rbs +4 -4
  60. metadata +14 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d95b9f470645015a3308ade4ab2349375eddd0598f6919fbf063d4feba61926c
4
- data.tar.gz: 95518fc5601eb0ba9a22e13814f9f0a339addca71e94c33122ed211780049ca4
3
+ metadata.gz: 7e5ee54988be76a44ae512da359d83c502f8a43073f244a116a8fdc45fa7b87d
4
+ data.tar.gz: e3c08652a8d08eadbd1ef14b20005f93ff4131a712f8234c3eab94c7fce07167
5
5
  SHA512:
6
- metadata.gz: 729bfc7fc5888f6872ecf9dfdcaefa6df68ba96b705cf1ccd1e8b9b866d25bf2c3081577b3c1828b99556009fc686c0705eb30dcb6fd5f2a3fb951128e0c621e
7
- data.tar.gz: 1173ae3cd242caf251c099e14db090efc3ac3524d8e3f63e04c331f2938a6d747c9874999d79dbc4790894fc8c447c900740235a62cb0376f2b124dad3674acc
6
+ metadata.gz: 58f16e523d23215d89a8c873a5ce12491b52726073c334c4d9800e17e40dca8111ad43ab2467c37939a19bebbaa000c8db144fdbba87cc3d22cb699687df6699
7
+ data.tar.gz: 64dd9bb70af4173339019b62fa9f54fdf3f22c4bf593b51fb3de624844ac8a599a51b49cae10623aee8b8cff24af4dee69e39bedfec7da3d1ae229d733632e9c
data/README.md CHANGED
@@ -61,7 +61,7 @@ puts body #=> #<HTTPX::Response ...
61
61
  You can also send as many requests as you want simultaneously:
62
62
 
63
63
  ```ruby
64
- page1, page2, page3 =`HTTPX.get("https://news.ycombinator.com/news", "https://news.ycombinator.com/news?p=2", "https://news.ycombinator.com/news?p=3")
64
+ page1, page2, page3 = HTTPX.get("https://news.ycombinator.com/news", "https://news.ycombinator.com/news?p=2", "https://news.ycombinator.com/news?p=3")
65
65
  ```
66
66
 
67
67
  ## Installation
@@ -107,22 +107,22 @@ HTTPX.get(
107
107
 
108
108
  ```ruby
109
109
  response = HTTPX.get("https://www.google.com", params: { q: "me" })
110
- response = HTTPX.post("https://www.nghttp2.org/httpbin/post", form: {name: "John", age: "22"})
110
+ response = HTTPX.post("https://www.nghttp2.org/httpbin/post", form: { name: "John", age: "22" })
111
111
  response = HTTPX.plugin(:basic_auth)
112
112
  .basic_auth("user", "pass")
113
113
  .get("https://www.google.com")
114
114
 
115
115
  # more complex client objects can be cached, and are thread-safe
116
- http = HTTPX.plugin(:expect).with(headers: { "x-pvt-token" => "TOKEN"})
116
+ http = HTTPX.plugin(:expect).with(headers: { "x-pvt-token" => "TOKEN" })
117
117
  http.get("https://example.com") # the above options will apply
118
- http.post("https://example2.com", form: {name: "John", age: "22"}) # same, plus the form POST body
118
+ http.post("https://example2.com", form: { name: "John", age: "22" }) # same, plus the form POST body
119
119
  ```
120
120
 
121
121
  ### Lightweight
122
122
 
123
123
  It ships with most features published as a plugin, making vanilla `httpx` lightweight and dependency-free, while allowing you to "pay for what you use"
124
124
 
125
- The plugin system is similar to the ones used by [sequel](https://github.com/jeremyevans/sequel), [roda](https://github.com/jeremyevans/roda) or [shrine](https://github.com/janko-m/shrine).
125
+ The plugin system is similar to the ones used by [sequel](https://github.com/jeremyevans/sequel), [roda](https://github.com/jeremyevans/roda) or [shrine](https://github.com/shrinerb/shrine).
126
126
 
127
127
  ### Advanced DNS features
128
128
 
@@ -136,7 +136,7 @@ The test suite runs against [httpbin proxied over nghttp2](https://nghttp2.org/h
136
136
 
137
137
  All Rubies greater or equal to 2.7, and always latest JRuby and Truffleruby.
138
138
 
139
- **Note**: This gem is tested against all latest patch versions, i.e. if you're using 3.2.0 and you experience some issue, please test it against 3.2.$latest before creating an issue.
139
+ **Note**: This gem is tested against all latest patch versions, i.e. if you're using 3.3.0 and you experience some issue, please test it against 3.3.$latest before creating an issue.
140
140
 
141
141
  ## Resources
142
142
  | | |
@@ -2,13 +2,13 @@
2
2
 
3
3
  ## improvements
4
4
 
5
- * (Re-)enabling default retries in DNS name queries; this had been disabled as a result of revamping timouts, and resulted in queries only being sent once, which is very little for UDP-related traffic, and breaks if using DNs rate-limiting software. Retries the query just once, for now.
5
+ * (Re-)enabling default retries in DNS name queries; this had been disabled as a result of revamping timeouts, and resulted in queries only being sent once, which is very little for UDP-related traffic, and breaks if using DNs rate-limiting software. Retries the query just once, for now.
6
6
 
7
7
  ## bugfixes
8
8
 
9
9
  * reset timers when adding new intervals, as these may be added as a result on after-select connection handling, and must wait for the next tick cycle (before the patch, they were triggering too soon).
10
10
  * fixed "on close" callback leak on connection reuse, which caused linear performance regression in benchmarks performing one request per connection.
11
- * fixed hanging connection whan an HTTP/1.1 emitted a "connection: close" header but the server would not emit one (it closes the connection now).
11
+ * fixed hanging connection when an HTTP/1.1 emitted a "connection: close" header but the server would not emit one (it closes the connection now).
12
12
  * fixed recursive dns cached lookups which may have already expired, and created nil entries in the returned address list.
13
13
  * dns system resolver is now able to retry on failure.
14
14
 
@@ -0,0 +1,49 @@
1
+ # 1.2.0
2
+
3
+ ## Features
4
+
5
+ ### `:ssrf_filter` plugin
6
+
7
+ The `:ssrf_filter` plugin prevents server-side request forgery attacks, by blocking requests to the internal network. This is useful when the URLs used to perform requests aren’t under the developer control (such as when they are inserted via a web application form).
8
+
9
+ ```ruby
10
+ http = HTTPX.plugin(:ssrf_filter)
11
+
12
+ # this works
13
+ response = http.get("https://example.com")
14
+
15
+ # this doesn't
16
+ response = http.get("http://localhost:3002")
17
+ response = http.get("http://[::1]:3002")
18
+ response = http.get("http://169.254.169.254/latest/meta-data/")
19
+ ```
20
+
21
+ More info under https://honeyryderchuck.gitlab.io/httpx/wiki/SSRF-Filter
22
+
23
+ ### `:callbacks` plugin
24
+
25
+ The session callbacks introduced in v0.24.0 are in its own plugin. Older code will still work and emit a deprecation warning.
26
+
27
+ More info under https://honeyryderchuck.gitlab.io/httpx/wiki/Callbacks
28
+
29
+ ### `:redirect_on` option for `:follow_redirects` plugin
30
+
31
+ This option allows passing a callback which, when returning `false`, can interrupt the redirect loop.
32
+
33
+ ```ruby
34
+ http = HTTPX.plugin(:follow_redirects).with(redirect_on: ->(location_uri) { BLACKLIST_HOSTS.include?(location_uri.host) })
35
+ ```
36
+
37
+ ### `:close_on_handshake_timeout` timeout
38
+
39
+ A new `:timeout` option, `:close_handshake_timeout`, is added, which monitors connection readiness when performing HTTP/2 connection termination handshake.
40
+
41
+ ## Improvements
42
+
43
+ * Internal "eden connections" concept was removed, and connection objects are now kept-and-reused during the lifetime of a session, even when closed. This simplified connectio pool implementation and improved performance.
44
+ * request using `:proxy` and `:retries` plugin enabled sessions will now retry on proxy connection establishment related errors.
45
+
46
+ ## Bugfixes
47
+
48
+ * webmock adapter: mocked responses storing decoded payloads won't try to decode them again (fixes vcr/webmock integrations).
49
+ * webmock adapter: fix issue related with making real requests over webmock-enabled connection.
@@ -0,0 +1,6 @@
1
+ # 1.2.1
2
+
3
+ ## Bugfixes
4
+
5
+ * DoH resolver: try resolving other candidates on "domain not found" error (same behaviour as with native resolver).
6
+ * Allow HTTP/2 connections to exit cleanly when TLS session gets corrupted and termination handshake can't be performed.
@@ -41,7 +41,9 @@ module WebMock
41
41
  request.options.response_class.new(request,
42
42
  webmock_response.status[0],
43
43
  "2.0",
44
- webmock_response.headers)
44
+ webmock_response.headers).tap do |res|
45
+ res.mocked = true
46
+ end
45
47
  end
46
48
 
47
49
  def build_error_response(request, exception)
@@ -50,16 +52,36 @@ module WebMock
50
52
  end
51
53
 
52
54
  module InstanceMethods
53
- def build_connection(*)
55
+ def init_connection(*)
54
56
  connection = super
55
57
  connection.once(:unmock_connection) do
58
+ unless connection.addresses
59
+ connection.__send__(:callbacks)[:connect_error].clear
60
+ pool.__send__(:unregister_connection, connection)
61
+ end
56
62
  pool.__send__(:resolve_connection, connection)
57
- pool.__send__(:unregister_connection, connection) unless connection.addresses
58
63
  end
59
64
  connection
60
65
  end
61
66
  end
62
67
 
68
+ module ResponseMethods
69
+ attr_accessor :mocked
70
+
71
+ def initialize(*)
72
+ super
73
+ @mocked = false
74
+ end
75
+ end
76
+
77
+ module ResponseBodyMethods
78
+ def decode_chunk(chunk)
79
+ return chunk if @response.mocked
80
+
81
+ super
82
+ end
83
+ end
84
+
63
85
  module ConnectionMethods
64
86
  def initialize(*)
65
87
  super
data/lib/httpx/altsvc.rb CHANGED
@@ -4,6 +4,58 @@ require "strscan"
4
4
 
5
5
  module HTTPX
6
6
  module AltSvc
7
+ # makes connections able to accept requests destined to primary service.
8
+ module ConnectionMixin
9
+ using URIExtensions
10
+
11
+ def send(request)
12
+ request.headers["alt-used"] = @origin.authority if @parser && !@write_buffer.full? && match_altsvcs?(request.uri)
13
+
14
+ super
15
+ end
16
+
17
+ def match?(uri, options)
18
+ return false if !used? && (@state == :closing || @state == :closed)
19
+
20
+ match_altsvcs?(uri) && match_altsvc_options?(uri, options)
21
+ end
22
+
23
+ private
24
+
25
+ # checks if this is connection is an alternative service of
26
+ # +uri+
27
+ def match_altsvcs?(uri)
28
+ @origins.any? { |origin| altsvc_match?(uri, origin) } ||
29
+ AltSvc.cached_altsvc(@origin).any? do |altsvc|
30
+ origin = altsvc["origin"]
31
+ altsvc_match?(origin, uri.origin)
32
+ end
33
+ end
34
+
35
+ def match_altsvc_options?(uri, options)
36
+ return @options == options unless @options.ssl.all? do |k, v|
37
+ v == (k == :hostname ? uri.host : options.ssl[k])
38
+ end
39
+
40
+ @options.options_equals?(options, Options::REQUEST_BODY_IVARS + %i[@ssl])
41
+ end
42
+
43
+ def altsvc_match?(uri, other_uri)
44
+ other_uri = URI(other_uri)
45
+
46
+ uri.origin == other_uri.origin || begin
47
+ case uri.scheme
48
+ when "h2"
49
+ (other_uri.scheme == "https" || other_uri.scheme == "h2") &&
50
+ uri.host == other_uri.host &&
51
+ uri.port == other_uri.port
52
+ else
53
+ false
54
+ end
55
+ end
56
+ end
57
+ end
58
+
7
59
  @altsvc_mutex = Thread::Mutex.new
8
60
  @altsvcs = Hash.new { |h, k| h[k] = [] }
9
61
 
@@ -46,7 +98,7 @@ module HTTPX
46
98
 
47
99
  altsvc = response.headers["alt-svc"]
48
100
 
49
- # https://tools.ietf.org/html/rfc7838#section-3
101
+ # https://datatracker.ietf.org/doc/html/rfc7838#section-3
50
102
  # A field value containing the special value "clear" indicates that the
51
103
  # origin requests all alternatives for that origin to be invalidated
52
104
  # (including those specified in the same response, in case of an
@@ -99,7 +151,10 @@ module HTTPX
99
151
  end
100
152
 
101
153
  def parse_altsvc_origin(alt_proto, alt_origin)
102
- alt_scheme = parse_altsvc_scheme(alt_proto) or return
154
+ alt_scheme = parse_altsvc_scheme(alt_proto)
155
+
156
+ return unless alt_scheme
157
+
103
158
  alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
104
159
 
105
160
  URI.parse("#{alt_scheme}://#{alt_origin}")
data/lib/httpx/buffer.rb CHANGED
@@ -3,6 +3,14 @@
3
3
  require "forwardable"
4
4
 
5
5
  module HTTPX
6
+ # Internal class to abstract a string buffer, by wrapping a string and providing the
7
+ # minimum possible API and functionality required.
8
+ #
9
+ # buffer = Buffer.new(640)
10
+ # buffer.full? #=> false
11
+ # buffer << "aa"
12
+ # buffer.capacity #=> 638
13
+ #
6
14
  class Buffer
7
15
  extend Forwardable
8
16
 
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
+ # Session mixin, implements most of the APIs that the users call.
5
+ # delegates to a default session when extended.
4
6
  module Chainable
5
7
  %w[head get post put delete trace options connect patch].each do |meth|
6
8
  class_eval(<<-MOD, __FILE__, __LINE__ + 1)
@@ -10,19 +12,7 @@ module HTTPX
10
12
  MOD
11
13
  end
12
14
 
13
- %i[
14
- connection_opened connection_closed
15
- request_error
16
- request_started request_body_chunk request_completed
17
- response_started response_body_chunk response_completed
18
- ].each do |meth|
19
- class_eval(<<-MOD, __FILE__, __LINE__ + 1)
20
- def on_#{meth}(&blk) # def on_connection_opened(&blk)
21
- on(:#{meth}, &blk) # on(:connection_opened, &blk)
22
- end # end
23
- MOD
24
- end
25
-
15
+ # delegates to the default session (see HTTPX::Session#request).
26
16
  def request(*args, **options)
27
17
  branch(default_options).request(*args, **options)
28
18
  end
@@ -31,10 +21,12 @@ module HTTPX
31
21
  with(headers: { "accept" => String(type) })
32
22
  end
33
23
 
24
+ # delegates to the default session (see HTTPX::Session#wrap).
34
25
  def wrap(&blk)
35
26
  branch(default_options).wrap(&blk)
36
27
  end
37
28
 
29
+ # returns a new instance loaded with the +pl+ plugin and +options+.
38
30
  def plugin(pl, options = nil, &blk)
39
31
  klass = is_a?(S) ? self.class : Session
40
32
  klass = Class.new(klass)
@@ -42,44 +34,71 @@ module HTTPX
42
34
  klass.plugin(pl, options, &blk).new
43
35
  end
44
36
 
37
+ # returns a new instance loaded with +options+.
45
38
  def with(options, &blk)
46
39
  branch(default_options.merge(options), &blk)
47
40
  end
48
41
 
49
- protected
50
-
51
- def on(*args, &blk)
52
- branch(default_options).on(*args, &blk)
53
- end
54
-
55
42
  private
56
43
 
44
+ # returns default instance of HTTPX::Options.
57
45
  def default_options
58
46
  @options || Session.default_options
59
47
  end
60
48
 
49
+ # returns a default instance of HTTPX::Session.
61
50
  def branch(options, &blk)
62
51
  return self.class.new(options, &blk) if is_a?(S)
63
52
 
64
53
  Session.new(options, &blk)
65
54
  end
66
55
 
67
- def method_missing(meth, *args, **options)
68
- return super unless meth =~ /\Awith_(.+)/
56
+ def method_missing(meth, *args, **options, &blk)
57
+ case meth
58
+ when /\Awith_(.+)/
69
59
 
70
- option = Regexp.last_match(1)
60
+ option = Regexp.last_match(1)
71
61
 
72
- return super unless option
62
+ return super unless option
73
63
 
74
- with(option.to_sym => (args.first || options))
75
- end
64
+ with(option.to_sym => args.first || options)
65
+ when /\Aon_(.+)/
66
+ callback = Regexp.last_match(1)
76
67
 
77
- def respond_to_missing?(meth, *)
78
- return super unless meth =~ /\Awith_(.+)/
68
+ return super unless %w[
69
+ connection_opened connection_closed
70
+ request_error
71
+ request_started request_body_chunk request_completed
72
+ response_started response_body_chunk response_completed
73
+ ].include?(callback)
79
74
 
80
- option = Regexp.last_match(1)
75
+ warn "DEPRECATION WARNING: calling `.#{meth}` on plain HTTPX sessions is deprecated. " \
76
+ "Use HTTPX.plugin(:callbacks).#{meth} instead."
81
77
 
82
- default_options.respond_to?(option) || super
78
+ plugin(:callbacks).__send__(meth, *args, **options, &blk)
79
+ else
80
+ super
81
+ end
82
+ end
83
+
84
+ def respond_to_missing?(meth, *)
85
+ case meth
86
+ when /\Awith_(.+)/
87
+ option = Regexp.last_match(1)
88
+
89
+ default_options.respond_to?(option) || super
90
+ when /\Aon_(.+)/
91
+ callback = Regexp.last_match(1)
92
+
93
+ %w[
94
+ connection_opened connection_closed
95
+ request_error
96
+ request_started request_body_chunk request_completed
97
+ response_started response_body_chunk response_completed
98
+ ].include?(callback) || super
99
+ else
100
+ super
101
+ end
83
102
  end
84
103
  end
85
104
  end
@@ -12,6 +12,8 @@ module HTTPX
12
12
 
13
13
  attr_reader :pending, :requests
14
14
 
15
+ attr_accessor :max_concurrent_requests
16
+
15
17
  def initialize(buffer, options)
16
18
  @options = Options.new(options)
17
19
  @max_concurrent_requests = @options.max_concurrent_requests || MAX_REQUESTS
@@ -47,6 +49,7 @@ module HTTPX
47
49
  @max_requests = @options.max_requests || MAX_REQUESTS
48
50
  @parser.reset!
49
51
  @handshake_completed = false
52
+ @pending.concat(@requests) unless @requests.empty?
50
53
  end
51
54
 
52
55
  def close
@@ -218,6 +221,7 @@ module HTTPX
218
221
  end
219
222
 
220
223
  def ping
224
+ reset
221
225
  emit(:reset)
222
226
  emit(:exhausted)
223
227
  end
@@ -262,6 +266,7 @@ module HTTPX
262
266
 
263
267
  def disable
264
268
  disable_pipelining
269
+ reset
265
270
  emit(:reset)
266
271
  throw(:called)
267
272
  end
@@ -292,29 +297,31 @@ module HTTPX
292
297
  request.body.chunk!
293
298
  end
294
299
 
295
- connection = request.headers["connection"]
300
+ extra_headers = {}
296
301
 
297
- connection ||= if request.persistent?
298
- # when in a persistent connection, the request can't be at
299
- # the edge of a renegotiation
300
- if @requests.index(request) + 1 < @max_requests
301
- "keep-alive"
302
- else
303
- "close"
304
- end
305
- else
306
- # when it's not a persistent connection, it sets "Connection: close" always
307
- # on the last request of the possible batch (either allowed max requests,
308
- # or if smaller, the size of the batch itself)
309
- requests_limit = [@max_requests, @requests.size].min
310
- if request == @requests[requests_limit - 1]
311
- "close"
302
+ unless request.headers.key?("connection")
303
+ connection_value = if request.persistent?
304
+ # when in a persistent connection, the request can't be at
305
+ # the edge of a renegotiation
306
+ if @requests.index(request) + 1 < @max_requests
307
+ "keep-alive"
308
+ else
309
+ "close"
310
+ end
312
311
  else
313
- "keep-alive"
312
+ # when it's not a persistent connection, it sets "Connection: close" always
313
+ # on the last request of the possible batch (either allowed max requests,
314
+ # or if smaller, the size of the batch itself)
315
+ requests_limit = [@max_requests, @requests.size].min
316
+ if request == @requests[requests_limit - 1]
317
+ "close"
318
+ else
319
+ "keep-alive"
320
+ end
314
321
  end
315
- end
316
322
 
317
- extra_headers = { "connection" => connection }
323
+ extra_headers["connection"] = connection_value
324
+ end
318
325
  extra_headers["host"] = request.authority unless request.headers.key?("host")
319
326
  extra_headers
320
327
  end
@@ -370,12 +377,10 @@ module HTTPX
370
377
  end
371
378
 
372
379
  def join_headers2(headers)
373
- buffer = "".b
374
380
  headers.each do |field, value|
375
- buffer << "#{capitalized(field)}: #{value}" << CRLF
381
+ buffer = "#{capitalized(field)}: #{value}#{CRLF}"
376
382
  log(color: :yellow) { "<- HEADER: #{buffer.chomp}" }
377
383
  @buffer << buffer
378
- buffer.clear
379
384
  end
380
385
  end
381
386
 
@@ -55,7 +55,7 @@ module HTTPX
55
55
  return :w
56
56
  end
57
57
 
58
- unless (@connection.state == :connected && @handshake_completed)
58
+ unless @connection.state == :connected && @handshake_completed
59
59
  return @buffer.empty? ? :r : :rw
60
60
  end
61
61
 
@@ -73,8 +73,11 @@ module HTTPX
73
73
  end
74
74
 
75
75
  def close
76
- @connection.goaway unless @connection.state == :closed
77
- emit(:close)
76
+ unless @connection.state == :closed
77
+ @connection.goaway
78
+ emit(:timeout, @options.timeout[:close_handshake_timeout])
79
+ end
80
+ emit(:close, true)
78
81
  end
79
82
 
80
83
  def empty?
@@ -147,6 +150,7 @@ module HTTPX
147
150
 
148
151
  def send_pending
149
152
  while (request = @pending.shift)
153
+ # TODO: this request should go back to top of stack
150
154
  break unless send(request)
151
155
  end
152
156
  end