httpx 0.16.1 → 0.18.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  3. data/doc/release_notes/0_17_0.md +49 -0
  4. data/doc/release_notes/0_18_0.md +69 -0
  5. data/doc/release_notes/0_18_1.md +12 -0
  6. data/doc/release_notes/0_18_2.md +10 -0
  7. data/lib/httpx/adapters/datadog.rb +1 -1
  8. data/lib/httpx/adapters/faraday.rb +5 -3
  9. data/lib/httpx/adapters/webmock.rb +9 -3
  10. data/lib/httpx/altsvc.rb +2 -2
  11. data/lib/httpx/chainable.rb +4 -4
  12. data/lib/httpx/connection/http1.rb +23 -14
  13. data/lib/httpx/connection/http2.rb +35 -17
  14. data/lib/httpx/connection.rb +74 -76
  15. data/lib/httpx/domain_name.rb +1 -1
  16. data/lib/httpx/extensions.rb +50 -4
  17. data/lib/httpx/headers.rb +1 -1
  18. data/lib/httpx/io/ssl.rb +5 -1
  19. data/lib/httpx/io/tls.rb +7 -7
  20. data/lib/httpx/loggable.rb +5 -5
  21. data/lib/httpx/options.rb +35 -13
  22. data/lib/httpx/parser/http1.rb +10 -6
  23. data/lib/httpx/plugins/aws_sdk_authentication.rb +42 -18
  24. data/lib/httpx/plugins/aws_sigv4.rb +9 -11
  25. data/lib/httpx/plugins/compression.rb +5 -3
  26. data/lib/httpx/plugins/cookies/jar.rb +1 -1
  27. data/lib/httpx/plugins/digest_authentication.rb +4 -4
  28. data/lib/httpx/plugins/expect.rb +7 -3
  29. data/lib/httpx/plugins/grpc/message.rb +2 -2
  30. data/lib/httpx/plugins/grpc.rb +3 -3
  31. data/lib/httpx/plugins/h2c.rb +7 -3
  32. data/lib/httpx/plugins/internal_telemetry.rb +8 -8
  33. data/lib/httpx/plugins/multipart/decoder.rb +187 -0
  34. data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
  35. data/lib/httpx/plugins/multipart/part.rb +2 -2
  36. data/lib/httpx/plugins/multipart.rb +16 -2
  37. data/lib/httpx/plugins/ntlm_authentication.rb +4 -4
  38. data/lib/httpx/plugins/proxy/ssh.rb +11 -4
  39. data/lib/httpx/plugins/proxy.rb +6 -4
  40. data/lib/httpx/plugins/response_cache/store.rb +55 -0
  41. data/lib/httpx/plugins/response_cache.rb +88 -0
  42. data/lib/httpx/plugins/retries.rb +36 -14
  43. data/lib/httpx/plugins/stream.rb +3 -4
  44. data/lib/httpx/pool.rb +39 -13
  45. data/lib/httpx/registry.rb +1 -1
  46. data/lib/httpx/request.rb +12 -13
  47. data/lib/httpx/resolver/https.rb +5 -7
  48. data/lib/httpx/resolver/native.rb +4 -2
  49. data/lib/httpx/resolver/resolver_mixin.rb +2 -1
  50. data/lib/httpx/resolver/system.rb +2 -0
  51. data/lib/httpx/resolver.rb +2 -2
  52. data/lib/httpx/response.rb +60 -44
  53. data/lib/httpx/selector.rb +16 -19
  54. data/lib/httpx/session.rb +22 -15
  55. data/lib/httpx/session2.rb +1 -1
  56. data/lib/httpx/timers.rb +84 -0
  57. data/lib/httpx/transcoder/body.rb +2 -1
  58. data/lib/httpx/transcoder/form.rb +20 -0
  59. data/lib/httpx/transcoder/json.rb +12 -0
  60. data/lib/httpx/transcoder.rb +62 -1
  61. data/lib/httpx/utils.rb +10 -2
  62. data/lib/httpx/version.rb +1 -1
  63. data/lib/httpx.rb +1 -0
  64. data/sig/buffer.rbs +2 -2
  65. data/sig/chainable.rbs +7 -1
  66. data/sig/connection/http1.rbs +15 -4
  67. data/sig/connection/http2.rbs +19 -5
  68. data/sig/connection.rbs +15 -9
  69. data/sig/headers.rbs +19 -18
  70. data/sig/options.rbs +13 -5
  71. data/sig/parser/http1.rbs +3 -3
  72. data/sig/plugins/aws_sdk_authentication.rbs +22 -4
  73. data/sig/plugins/aws_sigv4.rbs +12 -3
  74. data/sig/plugins/basic_authentication.rbs +1 -1
  75. data/sig/plugins/multipart.rbs +64 -8
  76. data/sig/plugins/proxy.rbs +6 -6
  77. data/sig/plugins/response_cache.rbs +35 -0
  78. data/sig/plugins/retries.rbs +3 -0
  79. data/sig/pool.rbs +6 -0
  80. data/sig/request.rbs +11 -8
  81. data/sig/resolver/native.rbs +2 -1
  82. data/sig/resolver/resolver_mixin.rbs +1 -1
  83. data/sig/resolver/system.rbs +3 -1
  84. data/sig/response.rbs +11 -4
  85. data/sig/selector.rbs +8 -6
  86. data/sig/session.rbs +8 -14
  87. data/sig/timers.rbs +32 -0
  88. data/sig/transcoder/form.rbs +1 -0
  89. data/sig/transcoder/json.rbs +1 -0
  90. data/sig/transcoder.rbs +5 -4
  91. data/sig/utils.rbs +4 -0
  92. metadata +18 -17
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 005e6856a12b549480cb2fcf8becd893e041026e9df634fa102fc410ab3527ef
4
- data.tar.gz: 00722d3ffe0822eb6f434df84db78e9ffb9b53cbd103220546dc611f3763117c
3
+ metadata.gz: e2f0ac68d969d4462c03c28fa818fa055e4e3aabad5a599197df6cc912274673
4
+ data.tar.gz: '069b5100a14183e79a968f4bb69363304ce046d098d1283f934568ea0fc23e06'
5
5
  SHA512:
6
- metadata.gz: e8c2c8c1ff44c845a9773e79b3564ed7491cbbcbfd6aa20ea8e93a7a4a029f5afe177c53693173b493efe54122324448ee14e34807aa37db77d3699cc7bb75e2
7
- data.tar.gz: bad8e79d228c37dd3efafcf530ba7188e4759e5aca807c7b693a7102b6a2bc339663bade32d78c80a62e691f8a8378731f864639965554ab78f4f031994f0626
6
+ metadata.gz: bddcf91c4cd851204ff789798bc8745e6e7f2b39face3ae77c83de4aa8be4de278f24a0ce1405f5e142f2f358fbf78472a5a816ae315ac43574c81adb6b82838
7
+ data.tar.gz: f9d992536ae6e464833dbe05c6a884ba373f84d90e42657bd18bab9fc69288561361041563dbe26dee92e9b9f337ee57d7eb7b9f7586abe705905184cd57f9e3
data/README.md CHANGED
@@ -45,7 +45,7 @@ response = HTTPX.get("https://nghttp2.org")
45
45
  puts response.status #=> 200
46
46
  body = response.body
47
47
  puts body #=> #<HTTPX::Response ...
48
- ```
48
+ ```
49
49
 
50
50
  You can also send as many requests as you want simultaneously:
51
51
 
@@ -79,7 +79,7 @@ In Ruby, HTTP client implementations are a known cheap commodity. Why this one?
79
79
 
80
80
  ### Concurrency
81
81
 
82
- This library supports HTTP/2 seamlessly (which means, if the request is secure, and the server support ALPN negotiation AND HTTP/2, the request will be made through HTTP/2). If you pass multiple URIs, and they can utilize the same connection, they will run concurrently in it.
82
+ This library supports HTTP/2 seamlessly (which means, if the request is secure, and the server support ALPN negotiation AND HTTP/2, the request will be made through HTTP/2). If you pass multiple URIs, and they can utilize the same connection, they will run concurrently in it.
83
83
 
84
84
  However if the server supports HTTP/1.1, it will try to use HTTP pipelining, falling back to 1 request at a time if the server doesn't support it (if the server support Keep-Alive connections, it will reuse the same connection).
85
85
 
@@ -137,7 +137,8 @@ In order to use HTTP/2 under JRuby, [check this link](https://gitlab.com/honeyry
137
137
 
138
138
  ### Known bugs
139
139
 
140
- Doesn't work with ruby 2.4.0 for Windows (see [#36](https://gitlab.com/honeyryderchuck/httpx/issues/36)).
140
+ * Doesn't work with ruby 2.4.0 for Windows (see [#36](https://gitlab.com/honeyryderchuck/httpx/issues/36)).
141
+ * Using `total_timeout` along with the `:persistent` plugin [does not work as you might expect](https://gitlab.com/honeyryderchuck/httpx/-/wikis/Timeouts#total_timeout).
141
142
 
142
143
  ## Contributing
143
144
 
@@ -0,0 +1,49 @@
1
+ # 0.17.0
2
+
3
+ ## Features
4
+
5
+ ### Response mime type decoders (#json, #form)
6
+
7
+ https://gitlab.com/honeyryderchuck/httpx/-/wikis/Response-Handling#response-decoding
8
+
9
+ Two new methods, `#json` and `#form`, were added to `HTTPX::Response`. As the name implies, they'll decode the raw payload into ruby objects you can work with.
10
+
11
+ ```ruby
12
+ # after HTTPX.get("https://api.smth/endpoint-returning-json")
13
+ response.json # same as JSON.dump(response.to_s)
14
+ ```
15
+
16
+ Although not yet documented, integrating custom decoders is also possible (i.e. parsing HTML with `nokogiri` or something similar).
17
+
18
+ ## Improvements
19
+
20
+ ### Connection: reduce interest calculations
21
+
22
+ Due to it being an intensive task, internal interest calculation in connections was reduce to the bare minimum.
23
+
24
+ ### Immutable Options, internal recycling of instances, improves memory usage in the happy path
25
+
26
+ A lot of effort went into avoiding generating options objects internally whenever necessary. This means, when sending several requests with the same set of options (the most common case in `httpx` usage), internally only one object is passed around. For that, the following improvements were done:
27
+
28
+ * `Options#merge` returns the same options the the options being merged are a subset of the current set of options (b126938a6547e09b726dd64298fb488891d938e9).
29
+ * `Session#build_request` bypasses instantiation of options if it receives an `Options` object (which happens internally in the happy path, if users don't call `#build_request` directly) (3d549817cb41d4b904102fdc61afe3ecd9170893).
30
+ * Improving internal `Session` APIs to not pass around options, and instead rely on accessing request options.
31
+ * `Options#to_hash` does not build internal garbage arrays anymore (cc02679b804f63798f5d2136a039be1624e96ab6).
32
+
33
+ ### Reduce regexp operations in the HTTP/1 parser
34
+
35
+ Some code paths in the HTTP/1 parser still using regular expressions were replaced by string operations accomplishing the same.
36
+
37
+ ### HTTP/1 improvements on the complexity of connection accounting calculations
38
+
39
+ Managing open HTTP/1 connections relies on operations calculating whether there are requests waiting for completion. This relied on traversing all requests for that connectionn (O(n)); it now only checks the completion state of the first and last request of that connection, given that all requests in HTTP/1 are sequential (O(1)); this optimization brings a big improvement to persistent and pipelined requests (65261217b1270913e4bb93717e8b8dcfa775565a).
40
+
41
+ ## Bugfixes
42
+
43
+ * fixing HTTP/1 protocol uncompliant exposing multiple values for the "Host" header (e435dd0534314508262184fb03d83124d89d2079).
44
+
45
+ * Custom response finalizer introduced in 0.16.0 has been reverted. It was brought to my attention that `Tempfile` implementation already takes care of the file on GC (and `httpx` was duplicating), and the approach taken in `httpx` was buggy in several ways (not tolerant to forks, never recycled finalizers...) (aa3be21c890f92a41afcc7931f01dd24cc801f7c).
46
+
47
+ ## Chore
48
+
49
+ RBS Typing improvements based on latest stdlib signatures additions, such as `openssl`, `digest`, `socket` and others.
@@ -0,0 +1,69 @@
1
+ # 0.18.0
2
+
3
+ ## Features
4
+
5
+ ### Response Cache
6
+
7
+ https://gitlab.com/honeyryderchuck/httpx/-/wikis/Response-Cache
8
+
9
+ The `:response_cache` plugin handles transparent usage of HTTP caching and conditional requests to improve performance and bandwidth usage.
10
+
11
+ ```ruby
12
+ client = HTTPX.plugin(:response_cache)
13
+ r1 = client.get("https://nghttp2.org/httpbin/cache")
14
+ r2 = client.get("https://nghttp2.org/httpbin/cache")
15
+
16
+ r1.status #=> 200
17
+ r2.status #=> 304
18
+ r1.body == r2.body #=> true
19
+ ```
20
+
21
+ ### jitter on "retry-after"
22
+
23
+ On the `:retries` plugin, jitter calculation is now applied to the value in seconds defined by user after which a request should be retried (i.e. if `:retry_after` option is set to `2`, the retry interval may be `1.5422312` seconds, for example). This is important to avoid cases of synchronized "thundering herd", where server rejects requests, but they all get retried at the same time because the retry interval is exactly the same.
24
+
25
+ You can override the jitter calculation function by using the [:retry_jitter](https://gitlab.com/honeyryderchuck/httpx/-/wikis/Retries#retry_jitter) option:
26
+
27
+ ```ruby
28
+ HTTPX.plugin(:retries, retry_after: 2, retry_jitter: ->(interval) { interval + rand }) # interval is 3
29
+ ```
30
+
31
+ You can opt out of this by setting `HTTPX_NO_JITTER=1` environment variable.
32
+
33
+ ### Response#error
34
+
35
+ `HTTPX::Response#error` was added, to match `HTTPX::Response#error`. It returns an exception for 4xx/5xx responses (`HTTPX::HTTPError`), `nil` otherwise. It allows for end users to write such code:
36
+
37
+ ```ruby
38
+ if (response = HTTPX.get(uri)).error
39
+ # success
40
+ else
41
+ #error
42
+ end
43
+ ```
44
+
45
+ ## Improvements
46
+
47
+ * `webmock` adapter: added support for "stub_http_request#to_timeout" (https://gitlab.com/honeyryderchuck/httpx/-/merge_requests/165).
48
+
49
+ ## timers not a dependency
50
+
51
+ The functionality provided by the `timers` gem was replaced by a simpler custom implementation. Although powerful, its complexity was somewhat unnecessary for `httpx`'s simpler event loop, where user-defined timeouts are usually the same for a given batch of requests. The removal of `timers` reduces the number of dependencies to 1, which is `http-2-next` and is maintained by me.
52
+
53
+ ## AWS plugins
54
+
55
+ * `aws_sdk_authentication` plugin: removed implementation relying on `aws-sdk-s3`, replacing it with an `aws-sdk-core` relying implementation, which only uses credentials strategies and region discovery (the whole point of this SDK is to use a minimal subset of AWS SDK).
56
+
57
+
58
+ ## Bugfixes
59
+
60
+ * Fixed Error class declaration on response decoders when mime type is invalid (https://gitlab.com/honeyryderchuck/httpx/-/merge_requests/166).
61
+ * `ErrorResponse#to_s` now removes ANSI escape sequences from error backtraces.
62
+ * Persistent connections were kept around both in the pool and in the selector; the first is necessary, but the second caused busy loop scenarios all over; they are now removed when no requests are being handled.
63
+ * Connections which failed connection handshake were removed from the pool, but not from the selector list, causing busy loop scenarios in a few cases; this has been fixed.
64
+ * Fixed issue where HTTP/2 streams were being closed twice (and signaling it also twice), messing connection accounting in the pool.
65
+ * DoH resolver was always subscribed to the default thread "connection pool", which broke scenarios were session was patched to use its custom pool; it now ensures that it subscribes to the pool it was created in.
66
+ * `:aws_sigv4` plugin: removed require of `aws-sdk-s3`, left there by mistake (the whole point of the plugin is to run without the AWS SDK).
67
+ ## Chore
68
+
69
+ * `HTTPX::ErrorResponse#status` is now deprecated.
@@ -0,0 +1,12 @@
1
+ # 0.18.1
2
+
3
+ ## Bugfixes
4
+
5
+ * HTTP/1.1 pipelining logs were logging the previously-buffered requests all together for each triggered request, which created some confusion for users when reporting errors. This has been fixed.
6
+ * HTTP/2 coalescing is now skipped when performing TLS connections with VERIFY_NONE.
7
+ * HTTP/2 peer GOAWAY frames will now result in a (retryable) connection error, instead of being ignored and leaving a "ghost" connection behind.
8
+ * fixed total timeout call which was not raising the exception.
9
+
10
+ ## Chore
11
+
12
+ This gem now requires MFA-based gem releases.
@@ -0,0 +1,10 @@
1
+ # 0.18.2
2
+
3
+ ## Bugfixes
4
+
5
+ * A bug was reported and fixed, whereby a persistent connection with a `:total_timeout` set was triggering the timeout and leaving the process looping indefinitely.
6
+
7
+
8
+ ## Chore
9
+
10
+ The quirk of using the `:persistent` plugin with `:total_timeout` has been documented: https://gitlab.com/honeyryderchuck/httpx/-/wikis/Timeouts#total_timeout.
@@ -64,7 +64,7 @@ module Datadog
64
64
  def finish(response)
65
65
  return unless @span
66
66
 
67
- if response.respond_to?(:error)
67
+ if response.is_a?(::HTTPX::ErrorResponse)
68
68
  @span.set_error(response.error)
69
69
  else
70
70
  @span.set_tag(Datadog::Ext::HTTP::STATUS_CODE, response.status.to_s)
@@ -22,6 +22,8 @@ module Faraday
22
22
  # :nocov:
23
23
 
24
24
  module RequestMixin
25
+ using ::HTTPX::HashExtensions
26
+
25
27
  private
26
28
 
27
29
  def build_request(env)
@@ -38,7 +40,7 @@ module Faraday
38
40
  timeout_options = {
39
41
  connect_timeout: env.request.open_timeout,
40
42
  operation_timeout: env.request.timeout,
41
- }.reject { |_, v| v.nil? }
43
+ }.compact
42
44
 
43
45
  options = {
44
46
  ssl: {},
@@ -101,7 +103,7 @@ module Faraday
101
103
  end
102
104
 
103
105
  def on_response(&blk)
104
- if block_given?
106
+ if blk
105
107
  @on_response = lambda do |response|
106
108
  blk.call(response)
107
109
  end
@@ -112,7 +114,7 @@ module Faraday
112
114
  end
113
115
 
114
116
  def on_complete(&blk)
115
- if block_given?
117
+ if blk
116
118
  @on_complete = blk
117
119
  self
118
120
  else
@@ -19,7 +19,7 @@ module WebMock
19
19
  module InstanceMethods
20
20
  private
21
21
 
22
- def send_requests(*requests, options)
22
+ def send_requests(*requests)
23
23
  request_signatures = requests.map do |request|
24
24
  request_signature = _build_webmock_request_signature(request)
25
25
  WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)
@@ -47,7 +47,7 @@ module WebMock
47
47
 
48
48
  unless real_requests.empty?
49
49
  reqs = real_requests.keys
50
- reqs.zip(super(*reqs, options)).each do |req, res|
50
+ reqs.zip(super(*reqs)).each do |req, res|
51
51
  idx = real_requests[req]
52
52
 
53
53
  if WebMock::CallbackRegistry.any_callbacks?
@@ -86,7 +86,9 @@ module WebMock
86
86
  end
87
87
 
88
88
  def _build_from_webmock_response(request, webmock_response)
89
- return ErrorResponse.new(request, webmock_response.exception, request.options) if webmock_response.exception
89
+ return _build_error_response(request, HTTPX::TimeoutError.new(1, "Timed out")) if webmock_response.should_timeout
90
+
91
+ return _build_error_response(request, webmock_response.exception) if webmock_response.exception
90
92
 
91
93
  response = request.options.response_class.new(request,
92
94
  webmock_response.status[0],
@@ -95,6 +97,10 @@ module WebMock
95
97
  response << webmock_response.body.dup
96
98
  response
97
99
  end
100
+
101
+ def _build_error_response(request, exception)
102
+ HTTPX::ErrorResponse.new(request, exception, request.options)
103
+ end
98
104
  end
99
105
  end
100
106
 
data/lib/httpx/altsvc.rb CHANGED
@@ -10,14 +10,14 @@ module HTTPX
10
10
  module_function
11
11
 
12
12
  def cached_altsvc(origin)
13
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
13
+ now = Utils.now
14
14
  @altsvc_mutex.synchronize do
15
15
  lookup(origin, now)
16
16
  end
17
17
  end
18
18
 
19
19
  def cached_altsvc_set(origin, entry)
20
- now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
20
+ now = Utils.now
21
21
  @altsvc_mutex.synchronize do
22
22
  return if @altsvcs[origin].any? { |altsvc| altsvc["origin"] == entry["origin"] }
23
23
 
@@ -4,9 +4,9 @@ module HTTPX
4
4
  module Chainable
5
5
  %i[head get post put delete trace options connect patch].each do |meth|
6
6
  class_eval(<<-MOD, __FILE__, __LINE__ + 1)
7
- def #{meth}(*uri, **options)
8
- request(:#{meth}, uri, **options)
9
- end
7
+ def #{meth}(*uri, **options) # def get(*uri, **options)
8
+ request(:#{meth}, uri, **options) # request(:get, uri, **options)
9
+ end # end
10
10
  MOD
11
11
  end
12
12
 
@@ -78,7 +78,7 @@ module HTTPX
78
78
  with(option.to_sym => (args.first || options))
79
79
  end
80
80
 
81
- def respond_to_missing?(meth)
81
+ def respond_to_missing?(meth, *)
82
82
  return super unless meth =~ /\Awith_(.+)/
83
83
 
84
84
  option = Regexp.last_match(1)
@@ -36,6 +36,8 @@ module HTTPX
36
36
 
37
37
  request = @requests.first
38
38
 
39
+ return unless request
40
+
39
41
  return :w if request.interests == :w || !@buffer.empty?
40
42
 
41
43
  :r
@@ -59,7 +61,12 @@ module HTTPX
59
61
  def empty?
60
62
  # this means that for every request there's an available
61
63
  # partial response, so there are no in-flight requests waiting.
62
- @requests.empty? || @requests.all? { |request| !request.response.nil? }
64
+ @requests.empty? || (
65
+ # checking all responses can be time-consuming. Alas, as in HTTP/1, responses
66
+ # do not come out of order, we can get away with checking first and last.
67
+ !@requests.first.response.nil? &&
68
+ (@requests.size == 1 || !@requests.last.response.nil?)
69
+ )
63
70
  end
64
71
 
65
72
  def <<(data)
@@ -260,7 +267,7 @@ module HTTPX
260
267
  def set_protocol_headers(request)
261
268
  if !request.headers.key?("content-length") &&
262
269
  request.body.bytesize == Float::INFINITY
263
- request.chunk!
270
+ request.body.chunk!
264
271
  end
265
272
 
266
273
  connection = request.headers["connection"]
@@ -278,17 +285,16 @@ module HTTPX
278
285
  # on the last request of the possible batch (either allowed max requests,
279
286
  # or if smaller, the size of the batch itself)
280
287
  requests_limit = [@max_requests, @requests.size].min
281
- if request != @requests[requests_limit - 1]
282
- "keep-alive"
283
- else
288
+ if request == @requests[requests_limit - 1]
284
289
  "close"
290
+ else
291
+ "keep-alive"
285
292
  end
286
293
  end
287
294
 
288
- {
289
- "host" => (request.headers["host"] || request.authority),
290
- "connection" => connection,
291
- }
295
+ extra_headers = { "connection" => connection }
296
+ extra_headers["host"] = request.authority unless request.headers.key?("host")
297
+ extra_headers
292
298
  end
293
299
 
294
300
  def headline_uri(request)
@@ -309,8 +315,9 @@ module HTTPX
309
315
  end
310
316
 
311
317
  def join_headers(request)
312
- @buffer << "#{request.verb.to_s.upcase} #{headline_uri(request)} HTTP/#{@version.join(".")}" << CRLF
313
- log(color: :yellow) { "<- HEADLINE: #{@buffer.to_s.chomp.inspect}" }
318
+ headline = "#{request.verb.to_s.upcase} #{headline_uri(request)} HTTP/#{@version.join(".")}"
319
+ @buffer << headline << CRLF
320
+ log(color: :yellow) { "<- HEADLINE: #{headline.chomp.inspect}" }
314
321
  extra_headers = set_protocol_headers(request)
315
322
  join_headers2(request.headers.each(extra_headers))
316
323
  log { "<- " }
@@ -318,7 +325,7 @@ module HTTPX
318
325
  end
319
326
 
320
327
  def join_body(request)
321
- return if request.empty?
328
+ return if request.body.empty?
322
329
 
323
330
  while (chunk = request.drain_body)
324
331
  log(color: :green) { "<- DATA: #{chunk.bytesize} bytes..." }
@@ -327,7 +334,9 @@ module HTTPX
327
334
  throw(:buffer_full, request) if @buffer.full?
328
335
  end
329
336
 
330
- raise request.drain_error if request.drain_error
337
+ return unless (error = request.drain_error)
338
+
339
+ raise error
331
340
  end
332
341
 
333
342
  def join_trailers(request)
@@ -354,7 +363,7 @@ module HTTPX
354
363
  }.freeze
355
364
 
356
365
  def capitalized(field)
357
- UPCASED[field] || field.to_s.split("-").map(&:capitalize).join("-")
366
+ UPCASED[field] || field.split("-").map(&:capitalize).join("-")
358
367
  end
359
368
  end
360
369
  Connection.register "http/1.1", Connection::HTTP1
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "securerandom"
4
- require "io/wait"
5
4
  require "http/2/next"
6
5
 
7
6
  module HTTPX
@@ -17,6 +16,12 @@ module HTTPX
17
16
  end
18
17
  end
19
18
 
19
+ class GoawayError < Error
20
+ def initialize
21
+ super(0, :no_error)
22
+ end
23
+ end
24
+
20
25
  attr_reader :streams, :pending
21
26
 
22
27
  def initialize(buffer, options)
@@ -56,7 +61,7 @@ module HTTPX
56
61
 
57
62
  return :w if !@pending.empty? && can_buffer_more_requests?
58
63
 
59
- return :w if @streams.each_key.any? { |r| r.interests == :w }
64
+ return :w unless @drains.empty?
60
65
 
61
66
  if @buffer.empty?
62
67
  return if @streams.empty? && @pings.empty?
@@ -218,7 +223,7 @@ module HTTPX
218
223
  log(level: 1, color: :yellow) do
219
224
  request.headers.merge(extra_headers).each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
220
225
  end
221
- stream.headers(request.headers.each(extra_headers), end_stream: request.empty?)
226
+ stream.headers(request.headers.each(extra_headers), end_stream: request.body.empty?)
222
227
  end
223
228
 
224
229
  def join_trailers(stream, request)
@@ -234,7 +239,7 @@ module HTTPX
234
239
  end
235
240
 
236
241
  def join_body(stream, request)
237
- return if request.empty?
242
+ return if request.body.empty?
238
243
 
239
244
  chunk = @drains.delete(request) || request.drain_body
240
245
  while chunk
@@ -249,7 +254,9 @@ module HTTPX
249
254
  chunk = next_chunk
250
255
  end
251
256
 
252
- on_stream_refuse(stream, request, request.drain_error) if request.drain_error
257
+ return unless (error = request.drain_error)
258
+
259
+ on_stream_refuse(stream, request, error)
253
260
  end
254
261
 
255
262
  ######
@@ -257,8 +264,10 @@ module HTTPX
257
264
  ######
258
265
 
259
266
  def on_stream_headers(stream, request, h)
260
- if request.response && request.response.version == "2.0"
261
- on_stream_trailers(stream, request, h)
267
+ response = request.response
268
+
269
+ if response.is_a?(Response) && response.version == "2.0"
270
+ on_stream_trailers(stream, response, h)
262
271
  return
263
272
  end
264
273
 
@@ -274,11 +283,11 @@ module HTTPX
274
283
  handle(request, stream) if request.expects?
275
284
  end
276
285
 
277
- def on_stream_trailers(stream, request, h)
286
+ def on_stream_trailers(stream, response, h)
278
287
  log(color: :yellow) do
279
288
  h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{v}" }.join("\n")
280
289
  end
281
- request.response.merge_headers(h)
290
+ response.merge_headers(h)
282
291
  end
283
292
 
284
293
  def on_stream_data(stream, request, data)
@@ -288,23 +297,25 @@ module HTTPX
288
297
  end
289
298
 
290
299
  def on_stream_refuse(stream, request, error)
291
- stream.close
292
300
  on_stream_close(stream, request, error)
301
+ stream.close
293
302
  end
294
303
 
295
304
  def on_stream_close(stream, request, error)
305
+ return if error == :stream_closed && !@streams.key?(request)
306
+
296
307
  log(level: 2) { "#{stream.id}: closing stream" }
297
308
  @drains.delete(request)
298
309
  @streams.delete(request)
299
310
 
300
- if error && error != :no_error
311
+ if error
301
312
  ex = Error.new(stream.id, error)
302
313
  ex.set_backtrace(caller)
303
314
  response = ErrorResponse.new(request, ex, request.options)
304
315
  emit(:response, request, response)
305
316
  else
306
317
  response = request.response
307
- if response.status == 421
318
+ if response && response.status == 421
308
319
  ex = MisdirectedRequestError.new(response)
309
320
  ex.set_backtrace(caller)
310
321
  emit(:error, request, ex)
@@ -339,9 +350,16 @@ module HTTPX
339
350
 
340
351
  def on_close(_last_frame, error, _payload)
341
352
  is_connection_closed = @connection.state == :closed
342
- if error && error != :no_error
353
+ if error
343
354
  @buffer.clear if is_connection_closed
344
- ex = Error.new(0, error)
355
+ if error == :no_error
356
+ ex = GoawayError.new
357
+ @pending.unshift(*@streams.keys)
358
+ @drains.clear
359
+ @streams.clear
360
+ else
361
+ ex = Error.new(0, error)
362
+ end
345
363
  ex.set_backtrace(caller)
346
364
  handle_error(ex)
347
365
  end
@@ -385,10 +403,10 @@ module HTTPX
385
403
  end
386
404
 
387
405
  def on_pong(ping)
388
- if !@pings.delete(ping.to_s)
389
- close(:protocol_error, "ping payload did not match")
390
- else
406
+ if @pings.delete(ping.to_s)
391
407
  emit(:pong)
408
+ else
409
+ close(:protocol_error, "ping payload did not match")
392
410
  end
393
411
  end
394
412
  end