httpx 0.18.0 → 0.19.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -4
  3. data/doc/release_notes/0_18_1.md +12 -0
  4. data/doc/release_notes/0_18_2.md +10 -0
  5. data/doc/release_notes/0_18_3.md +7 -0
  6. data/doc/release_notes/0_18_4.md +14 -0
  7. data/doc/release_notes/0_18_5.md +10 -0
  8. data/doc/release_notes/0_18_6.md +5 -0
  9. data/doc/release_notes/0_18_7.md +5 -0
  10. data/doc/release_notes/0_19_0.md +39 -0
  11. data/doc/release_notes/0_19_1.md +5 -0
  12. data/doc/release_notes/0_19_2.md +7 -0
  13. data/doc/release_notes/0_19_3.md +6 -0
  14. data/lib/httpx/adapters/faraday.rb +58 -12
  15. data/lib/httpx/adapters/webmock.rb +71 -59
  16. data/lib/httpx/altsvc.rb +25 -9
  17. data/lib/httpx/connection/http1.rb +10 -7
  18. data/lib/httpx/connection/http2.rb +23 -8
  19. data/lib/httpx/connection.rb +26 -12
  20. data/lib/httpx/extensions.rb +16 -0
  21. data/lib/httpx/headers.rb +0 -2
  22. data/lib/httpx/io/ssl.rb +4 -0
  23. data/lib/httpx/io/tcp.rb +27 -6
  24. data/lib/httpx/io/udp.rb +0 -1
  25. data/lib/httpx/options.rb +44 -11
  26. data/lib/httpx/plugins/cookies.rb +5 -7
  27. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  28. data/lib/httpx/plugins/multipart/mime_type_detector.rb +18 -4
  29. data/lib/httpx/plugins/proxy/http.rb +10 -23
  30. data/lib/httpx/plugins/proxy/socks4.rb +1 -1
  31. data/lib/httpx/plugins/proxy/socks5.rb +1 -1
  32. data/lib/httpx/plugins/proxy.rb +35 -15
  33. data/lib/httpx/plugins/retries.rb +15 -12
  34. data/lib/httpx/pool.rb +40 -20
  35. data/lib/httpx/request.rb +1 -1
  36. data/lib/httpx/resolver/https.rb +32 -42
  37. data/lib/httpx/resolver/multi.rb +79 -0
  38. data/lib/httpx/resolver/native.rb +28 -36
  39. data/lib/httpx/resolver/resolver.rb +95 -0
  40. data/lib/httpx/resolver/system.rb +175 -19
  41. data/lib/httpx/resolver.rb +37 -11
  42. data/lib/httpx/response.rb +4 -2
  43. data/lib/httpx/selector.rb +7 -0
  44. data/lib/httpx/session.rb +2 -16
  45. data/lib/httpx/session_extensions.rb +26 -0
  46. data/lib/httpx/timers.rb +1 -1
  47. data/lib/httpx/transcoder/chunker.rb +0 -1
  48. data/lib/httpx/version.rb +1 -1
  49. data/lib/httpx.rb +3 -0
  50. data/sig/connection/http1.rbs +5 -2
  51. data/sig/connection/http2.rbs +5 -2
  52. data/sig/connection.rbs +1 -0
  53. data/sig/errors.rbs +8 -0
  54. data/sig/headers.rbs +0 -2
  55. data/sig/httpx.rbs +4 -0
  56. data/sig/options.rbs +10 -7
  57. data/sig/parser/http1.rbs +14 -5
  58. data/sig/pool.rbs +17 -9
  59. data/sig/registry.rbs +3 -0
  60. data/sig/request.rbs +11 -0
  61. data/sig/resolver/https.rbs +15 -27
  62. data/sig/resolver/multi.rbs +7 -0
  63. data/sig/resolver/native.rbs +3 -12
  64. data/sig/resolver/resolver.rbs +36 -0
  65. data/sig/resolver/system.rbs +3 -9
  66. data/sig/resolver.rbs +12 -10
  67. data/sig/response.rbs +15 -5
  68. data/sig/selector.rbs +3 -3
  69. data/sig/timers.rbs +5 -2
  70. data/sig/transcoder/chunker.rbs +16 -5
  71. data/sig/transcoder/json.rbs +5 -0
  72. data/sig/transcoder.rbs +3 -1
  73. metadata +31 -5
  74. data/lib/httpx/resolver/resolver_mixin.rb +0 -75
  75. data/sig/resolver/resolver_mixin.rbs +0 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 372f89cdf05727a32d23467503299ae77831cb5d23356467dc0c7f3a22bbfd0c
4
- data.tar.gz: bbb3df9079bf4f5a449f37f34dd6b1d3208a978e8a8eaa7fdd69ea9d1829ac6c
3
+ metadata.gz: 431fcabfda42f5d6010d903a15c6a83357123c9eef3528a769755c8639f1ec61
4
+ data.tar.gz: 52512337b7b2081a4ab123f7261dee14adb97da019fc9244b9ddf8e7de53c904
5
5
  SHA512:
6
- metadata.gz: 38211af1e56fbc823ad0780198579ff11d4fa4b5b64677af95e78361f8b274d0dc43b124697358f1a0b17c2d45e4cdd620f97ca983af7fa60ce59515e9b51785
7
- data.tar.gz: 2c9d48f3ad7a499046ab94ac3ee1831f1623ce44990de0f08320ac6e5ce3767ca938381cd7b96c3799477f201a811aa5c446d62f88bd58c6e1e0afd84c53af75
6
+ metadata.gz: 7777af904a2e5a8f6f34984a69cb022c853483b6634dd631a3fbe92c9005fadb3637a21a214ff1e14ef4729ffa70d68362b703b9ccd25e2904b820fc674dbe6b
7
+ data.tar.gz: 94d92ad004319ecb3098b5e901921bfaf5d818cbb0e2e87eed8987248d1cc52d0fc70a5dc1dfe1a92faa8eae9e7074c926c0c01563002cb8067af449ae79fd51
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,13 +137,18 @@ 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).
142
+
143
+ ## Versioning Policy
144
+
145
+ Although 0.x software, `httpx` is considered API-stable and production-ready, i.e. current API or options may be subject to deprecation and emit log warnings, but can only effectively be removed in a major version change.
141
146
 
142
147
  ## Contributing
143
148
 
144
149
  * Discuss your contribution in an issue
145
150
  * Fork it
146
151
  * Make your changes, add some tests
147
- * Ensure all tests pass (`bundle exec rake test`)
152
+ * Ensure all tests pass (`docker-compose -f docker-compose.yml -f docker-compose-ruby-{RUBY_VERSION}.yml run httpx bundle exec rake test`)
148
153
  * Open a Merge Request (that's Pull Request in Github-ish)
149
154
  * Wait for feedback
@@ -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.
@@ -0,0 +1,7 @@
1
+ # 0.18.3
2
+
3
+ ## Bugfixes
4
+
5
+ * request bodies eager-loaded from enumerables yield duped partial chunks.
6
+
7
+ An error was observed while looking at webmock integration, where requests formed via the multipart plugin were returning an empty string as body. The issue was caused by an optimization on multipart encoder, which reuses the same buffer when reading chunks. Unfortunately, these cannot be yielded the same way via IO.copy_stream, as the same (cleared) buffer will be used to generate the eager-loaded body chunks.
@@ -0,0 +1,14 @@
1
+ # 0.18.4
2
+
3
+ ## Improvements
4
+
5
+ * faraday adapter: added support for `#on_data` callback in order to support [faraday streaming](https://lostisland.github.io/faraday/usage/streaming).
6
+
7
+ * multipart plugin: removed support for file mime type detection using `mime-types`. The reasoning behind it was that `mime-types` uses the filename, which is a very inaccurate detection strategy (ex: an mp4 video will be identified as `application/mp4`, instead of the correct `video/mp4`).
8
+ * multipart plugin: supported for file mime type detection using `marcel` and `filemagic` was added. Both use the magic header bytes, which is a more accurate strategy for file type detection.
9
+
10
+ ## Bugfixes
11
+
12
+ * webmock adapter has been reimplemented to work with `httpx` plugins (such as the `:retries` plugin). Some other fixes were applied to make it work better under `vcr` (a common `webmock` extension).
13
+
14
+ * fixed the URI-related bug which was making requests stall under ruby 3.1 (still not officially testing against it).
@@ -0,0 +1,10 @@
1
+ # 0.18.5
2
+
3
+ ## Improvements
4
+
5
+ * ruby 3.1 is now officially supported.
6
+ * when a user sets a `Host` header for an HTTP/2 request, this will be used in the `:authority` HTTP/2 pseudo-header, instead of silently ignored (mimicking what "curl" does).
7
+
8
+ ## Bugfixes
9
+
10
+ * fixed "throw outside of catch block" error happening when pipelining requests on an HTTP/1 connection and resulting in a timeout.
@@ -0,0 +1,5 @@
1
+ # 0.18.6
2
+
3
+ ## Bugfixes
4
+
5
+ * multipart plugin: fixed missing constant in `filemagic` integration.
@@ -0,0 +1,5 @@
1
+ # 0.18.6
2
+
3
+ ## Bugfixes
4
+
5
+ * multipart plugin: fixed `filemagic` integration by rewinding the file after mime-type detection.
@@ -0,0 +1,39 @@
1
+ # 0.19.0
2
+
3
+ ## Features
4
+
5
+ ### Happy Eyeballs v2
6
+
7
+ When the system supports dual-stack networking, `httpx` implements the Happy Eyeballs v2 algorithm (RFC 8305) to resolve hostnames to both IPv6 and IPv4 addresses while privileging IPv6 connectivity. This is implemented by `httpx` both for the `:native` as well as the `:https` (DoH) resolver (which do not perform address sorting, thereby being "DNS-based load-balancing" friendly), and "outsourced" to `getaddrinfo` when using the `:system` resolver.
8
+
9
+ IPv6 connectivity will also be privileged for `/etc/hosts` local DNS (i.e. `localhost` connections will connec to `::1`).
10
+
11
+ A new option, `:ip_families`, will also be available (`[Socket::AF_INET6, Socket::AF_INET]` in dual-stack systems). If you'd like to i.e. force IPv4 connectivity, you can do use it (`client = HTTPX.with(ip_families: [Socket::AF_INET])`).
12
+
13
+ ## Improvements
14
+
15
+ ### DNS: :system resolver uses getaddrinfo (instead of the resolver lib)
16
+
17
+ The `:system` resolver switched to using the `getaddinfo` system function to perform DNS requests. Not only is this call **not** blocking the session event loop anymore (unlike pre-0.19.0 `:system` resolver), it adds a lot of functionality that the stdlib `resolv` library just doesn't support at the moment (such as SRV records).
18
+
19
+ ### HTTP/2 proxy support
20
+
21
+ The `:proxy` plugin handles "prior-knowledge" HTTP/2 proxies.
22
+
23
+ ```ruby
24
+ HTTPX.plugin(:proxy, fallback_protocol: "h2").with_proxy(uri: "http://http2-proxy:3128").get(...
25
+ ```
26
+
27
+ Connection coalescing has also been enabled for proxied connections (also `CONNECT`-tunneled connections).
28
+
29
+ ### curl-to-httpx
30
+
31
+ widget in [project website](https://honeyryderchuck.gitlab.io/httpx/) to turn curl commands into the equivalent `httpx` code.
32
+
33
+ ## Bugfixes
34
+
35
+ * faraday adapter now supports passing session options.
36
+ * proxy: several fixes which enabled env-var (`HTTP(S)_PROXY`) defined proxy support.
37
+ * proxy: fixed graceful recovery from proxy tcp connect errors.
38
+ * several fixes around CNAMEs timeouts with the native resolver.
39
+ * https resolver is now closed when wrapping session closes (it was left open).
@@ -0,0 +1,5 @@
1
+ # 0.19.1
2
+
3
+ ## Bugfixes
4
+
5
+ Fixing a DNS dual-stack case where one the resolvers may have finished way before the previous one and will therefore return no timeout.
@@ -0,0 +1,7 @@
1
+ # 0.19.2
2
+
3
+ ## Bugfixes
4
+
5
+ * skip resolution delay path for early resolve cases
6
+
7
+ when the early resolve path (using IP, /etc/hosts IP, IP from cache) is followed, emit_addresses is called, and in a particular case (dual-stack network but using an IPv4 address), the happy eyeballs resolution delay path was activated when it shouldn't (it's meant to be used only for DNS network requests), and resulted in @pool being called before it was ever set. This simple check ensures that it doesn't happen before it must.
@@ -0,0 +1,6 @@
1
+ # 0.19.3
2
+
3
+ ## Bugfixes
4
+
5
+ * `retries` plugin: allow passing floats to `:retry_after` option.
6
+ * dns: fixing cache lookups filtering by IP family which was causing socket connect handshake to start with no IP.
@@ -21,6 +21,17 @@ module Faraday
21
21
  end
22
22
  # :nocov:
23
23
 
24
+ unless Faraday::RequestOptions.method_defined?(:stream_response?)
25
+ module RequestOptionsExtensions
26
+ refine Faraday::RequestOptions do
27
+ def stream_response?
28
+ false
29
+ end
30
+ end
31
+ end
32
+ using RequestOptionsExtensions
33
+ end
34
+
24
35
  module RequestMixin
25
36
  using ::HTTPX::HashExtensions
26
37
 
@@ -64,6 +75,30 @@ module Faraday
64
75
 
65
76
  include RequestMixin
66
77
 
78
+ module OnDataPlugin
79
+ module RequestMethods
80
+ attr_writer :response_on_data
81
+
82
+ def response=(response)
83
+ super
84
+
85
+ return if response.is_a?(::HTTPX::ErrorResponse)
86
+
87
+ response.body.on_data = @response_on_data
88
+ end
89
+ end
90
+
91
+ module ResponseBodyMethods
92
+ attr_writer :on_data
93
+
94
+ def write(chunk)
95
+ return super unless @on_data
96
+
97
+ @on_data.call(chunk, chunk.bytesize)
98
+ end
99
+ end
100
+ end
101
+
67
102
  class Session < ::HTTPX::Session
68
103
  plugin(:compression)
69
104
  plugin(:persistent)
@@ -104,7 +139,7 @@ module Faraday
104
139
 
105
140
  def on_response(&blk)
106
141
  if blk
107
- @on_response = lambda do |response|
142
+ @on_response = ->(response) do
108
143
  blk.call(response)
109
144
  end
110
145
  self
@@ -137,15 +172,21 @@ module Faraday
137
172
  end
138
173
 
139
174
  def run
140
- requests = @handlers.map { |handler| build_request(handler.env) }
141
175
  env = @handlers.last.env
142
176
 
143
- proxy_options = { uri: env.request.proxy }
144
-
145
177
  session = @session.with(options_from_env(env))
146
- session = session.plugin(:proxy).with(proxy: proxy_options) if env.request.proxy
178
+ session = session.plugin(:proxy).with(proxy: { uri: env.request.proxy }) if env.request.proxy
179
+ session = session.plugin(OnDataPlugin) if env.request.stream_response?
180
+
181
+ requests = @handlers.map { |handler| session.build_request(*build_request(handler.env)) }
147
182
 
148
- responses = session.request(requests)
183
+ if env.request.stream_response?
184
+ requests.each do |request|
185
+ request.response_on_data = env.request.on_data
186
+ end
187
+ end
188
+
189
+ responses = session.request(*requests)
149
190
  Array(responses).each_with_index do |response, index|
150
191
  handler = @handlers[index]
151
192
  handler.on_response.call(response)
@@ -162,9 +203,9 @@ module Faraday
162
203
  end
163
204
  end
164
205
 
165
- def initialize(app)
206
+ def initialize(app, options = {})
166
207
  super(app)
167
- @session = Session.new
208
+ @session = Session.new(options)
168
209
  end
169
210
 
170
211
  def call(env)
@@ -172,6 +213,7 @@ module Faraday
172
213
  if parallel?(env)
173
214
  handler = env[:parallel_manager].enqueue(env)
174
215
  handler.on_response do |response|
216
+ response.raise_for_status
175
217
  save_response(env, response.status, response.body.to_s, response.headers, response.reason) do |response_headers|
176
218
  response_headers.merge!(response.headers)
177
219
  end
@@ -179,11 +221,15 @@ module Faraday
179
221
  return handler
180
222
  end
181
223
 
182
- meth, uri, request_options = build_request(env)
183
-
184
224
  session = @session.with(options_from_env(env))
185
- session = session.plugin(:proxy).with(proxy: proxy_options) if env.request.proxy
186
- response = session.__send__(meth, uri, **request_options)
225
+ session = session.plugin(:proxy).with(proxy: { uri: env.request.proxy }) if env.request.proxy
226
+ session = session.plugin(OnDataPlugin) if env.request.stream_response?
227
+
228
+ request = session.build_request(*build_request(env))
229
+
230
+ request.response_on_data = env.request.on_data if env.request.stream_response?
231
+
232
+ response = session.request(request)
187
233
  response.raise_for_status unless response.is_a?(::HTTPX::Response)
188
234
  save_response(env, response.status, response.body.to_s, response.headers, response.reason) do |response_headers|
189
235
  response_headers.merge!(response.headers)
@@ -16,56 +16,8 @@ module WebMock
16
16
  # Requests are "hijacked" at the session, before they're distributed to a connection.
17
17
  #
18
18
  module Plugin
19
- module InstanceMethods
20
- private
21
-
22
- def send_requests(*requests)
23
- request_signatures = requests.map do |request|
24
- request_signature = _build_webmock_request_signature(request)
25
- WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)
26
- request_signature
27
- end
28
-
29
- responses = request_signatures.map do |request_signature|
30
- WebMock::StubRegistry.instance.response_for_request(request_signature)
31
- end
32
-
33
- real_requests = {}
34
-
35
- requests.each_with_index.each_with_object([request_signatures, responses]) do |(request, idx), (sig_reqs, mock_responses)|
36
- if (webmock_response = mock_responses[idx])
37
- mock_responses[idx] = _build_from_webmock_response(request, webmock_response)
38
- WebMock::CallbackRegistry.invoke_callbacks({ lib: :httpx }, sig_reqs[idx], webmock_response)
39
- log { "mocking #{request.uri} with #{mock_responses[idx].inspect}" }
40
- elsif WebMock.net_connect_allowed?(sig_reqs[idx].uri)
41
- log { "performing #{request.uri}" }
42
- real_requests[request] = idx
43
- else
44
- raise WebMock::NetConnectNotAllowedError, sig_reqs[idx]
45
- end
46
- end
47
-
48
- unless real_requests.empty?
49
- reqs = real_requests.keys
50
- reqs.zip(super(*reqs)).each do |req, res|
51
- idx = real_requests[req]
52
-
53
- if WebMock::CallbackRegistry.any_callbacks?
54
- webmock_response = _build_webmock_response(req, res)
55
- WebMock::CallbackRegistry.invoke_callbacks(
56
- { lib: :httpx, real_request: true }, request_signatures[idx],
57
- webmock_response
58
- )
59
- end
60
-
61
- responses[idx] = res
62
- end
63
- end
64
-
65
- responses
66
- end
67
-
68
- def _build_webmock_request_signature(request)
19
+ class << self
20
+ def build_webmock_request_signature(request)
69
21
  uri = WebMock::Util::URI.heuristic_parse(request.uri)
70
22
  uri.path = uri.normalized_path.gsub("[^:]//", "/")
71
23
 
@@ -77,7 +29,7 @@ module WebMock
77
29
  )
78
30
  end
79
31
 
80
- def _build_webmock_response(_request, response)
32
+ def build_webmock_response(_request, response)
81
33
  webmock_response = WebMock::Response.new
82
34
  webmock_response.status = [response.status, HTTP_REASONS[response.status]]
83
35
  webmock_response.body = response.body.to_s
@@ -85,10 +37,10 @@ module WebMock
85
37
  webmock_response
86
38
  end
87
39
 
88
- def _build_from_webmock_response(request, webmock_response)
89
- return _build_error_response(request, HTTPX::TimeoutError.new(1, "Timed out")) if webmock_response.should_timeout
40
+ def build_from_webmock_response(request, webmock_response)
41
+ return build_error_response(request, HTTPX::TimeoutError.new(1, "Timed out")) if webmock_response.should_timeout
90
42
 
91
- return _build_error_response(request, webmock_response.exception) if webmock_response.exception
43
+ return build_error_response(request, webmock_response.exception) if webmock_response.exception
92
44
 
93
45
  response = request.options.response_class.new(request,
94
46
  webmock_response.status[0],
@@ -98,10 +50,70 @@ module WebMock
98
50
  response
99
51
  end
100
52
 
101
- def _build_error_response(request, exception)
53
+ def build_error_response(request, exception)
102
54
  HTTPX::ErrorResponse.new(request, exception, request.options)
103
55
  end
104
56
  end
57
+
58
+ module InstanceMethods
59
+ def build_connection(*)
60
+ connection = super
61
+ connection.once(:unmock_connection) do
62
+ pool.__send__(:resolve_connection, connection)
63
+ pool.__send__(:unregister_connection, connection) unless connection.addresses
64
+ end
65
+ connection
66
+ end
67
+ end
68
+
69
+ module ConnectionMethods
70
+ def initialize(*)
71
+ super
72
+ @mocked = true
73
+ end
74
+
75
+ def open?
76
+ return true if @mocked
77
+
78
+ super
79
+ end
80
+
81
+ def interests
82
+ return if @mocked
83
+
84
+ super
85
+ end
86
+
87
+ def send(request)
88
+ request_signature = Plugin.build_webmock_request_signature(request)
89
+ WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)
90
+
91
+ if (mock_response = WebMock::StubRegistry.instance.response_for_request(request_signature))
92
+ response = Plugin.build_from_webmock_response(request, mock_response)
93
+ WebMock::CallbackRegistry.invoke_callbacks({ lib: :httpx }, request_signature, mock_response)
94
+ log { "mocking #{request.uri} with #{mock_response.inspect}" }
95
+ request.response = response
96
+ request.emit(:response, response)
97
+ elsif WebMock.net_connect_allowed?(request_signature.uri)
98
+ if WebMock::CallbackRegistry.any_callbacks?
99
+ request.on(:response) do |resp|
100
+ unless resp.is_a?(HTTPX::ErrorResponse)
101
+ webmock_response = Plugin.build_webmock_response(request, resp)
102
+ WebMock::CallbackRegistry.invoke_callbacks(
103
+ { lib: :httpx, real_request: true }, request_signature,
104
+ webmock_response
105
+ )
106
+ end
107
+ end
108
+ end
109
+ @mocked = false
110
+ emit(:unmock_connection, self)
111
+ super
112
+ else
113
+ raise WebMock::NetConnectNotAllowedError, request_signature
114
+ end
115
+ end
116
+ end
105
117
  end
106
118
 
107
119
  class HttpxAdapter < HttpLibAdapter
@@ -109,12 +121,12 @@ module WebMock
109
121
 
110
122
  class << self
111
123
  def enable!
112
- @original_session = ::HTTPX::Session
124
+ @original_session = HTTPX::Session
113
125
 
114
- webmock_session = ::HTTPX.plugin(Plugin)
126
+ webmock_session = HTTPX.plugin(Plugin)
115
127
 
116
- ::HTTPX.send(:remove_const, :Session)
117
- ::HTTPX.send(:const_set, :Session, webmock_session.class)
128
+ HTTPX.send(:remove_const, :Session)
129
+ HTTPX.send(:const_set, :Session, webmock_session.class)
118
130
  end
119
131
 
120
132
  def disable!
data/lib/httpx/altsvc.rb CHANGED
@@ -70,7 +70,7 @@ module HTTPX
70
70
 
71
71
  scanner = StringScanner.new(altsvc)
72
72
  until scanner.eos?
73
- alt_origin = scanner.scan(/[^=]+=("[^"]+"|[^;,]+)/)
73
+ alt_service = scanner.scan(/[^=]+=("[^"]+"|[^;,]+)/)
74
74
 
75
75
  alt_params = []
76
76
  loop do
@@ -80,29 +80,45 @@ module HTTPX
80
80
  break if scanner.eos? || scanner.scan(/ *, */)
81
81
  end
82
82
  alt_params = Hash[alt_params.map { |field| field.split("=") }]
83
- yield(parse_altsvc_origin(alt_origin), alt_params)
83
+
84
+ alt_proto, alt_authority = alt_service.split("=")
85
+ alt_origin = parse_altsvc_origin(alt_proto, alt_authority)
86
+ return unless alt_origin
87
+
88
+ yield(alt_origin, alt_params.merge("proto" => alt_proto))
89
+ end
90
+ end
91
+
92
+ def parse_altsvc_scheme(alt_proto)
93
+ case alt_proto
94
+ when "h2c"
95
+ "http"
96
+ when "h2"
97
+ "https"
84
98
  end
85
99
  end
86
100
 
87
101
  # :nocov:
88
102
  if RUBY_VERSION < "2.2"
89
- def parse_altsvc_origin(alt_origin)
90
- alt_proto, alt_origin = alt_origin.split("=")
103
+ def parse_altsvc_origin(alt_proto, alt_origin)
104
+ alt_scheme = parse_altsvc_scheme(alt_proto) or return
105
+
91
106
  alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
92
107
  if alt_origin.start_with?(":")
93
- alt_origin = "#{alt_proto}://dummy#{alt_origin}"
108
+ alt_origin = "#{alt_scheme}://dummy#{alt_origin}"
94
109
  uri = URI.parse(alt_origin)
95
110
  uri.host = nil
96
111
  uri
97
112
  else
98
- URI.parse("#{alt_proto}://#{alt_origin}")
113
+ URI.parse("#{alt_scheme}://#{alt_origin}")
99
114
  end
100
115
  end
101
116
  else
102
- def parse_altsvc_origin(alt_origin)
103
- alt_proto, alt_origin = alt_origin.split("=")
117
+ def parse_altsvc_origin(alt_proto, alt_origin)
118
+ alt_scheme = parse_altsvc_scheme(alt_proto) or return
104
119
  alt_origin = alt_origin[1..-2] if alt_origin.start_with?("\"") && alt_origin.end_with?("\"")
105
- URI.parse("#{alt_proto}://#{alt_origin}")
120
+
121
+ URI.parse("#{alt_scheme}://#{alt_origin}")
106
122
  end
107
123
  end
108
124
  # :nocov:
@@ -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
@@ -182,7 +184,7 @@ module HTTPX
182
184
  end
183
185
 
184
186
  if @pipelining
185
- disable
187
+ catch(:called) { disable }
186
188
  else
187
189
  @requests.each do |request|
188
190
  emit(:error, request, ex)
@@ -295,10 +297,6 @@ module HTTPX
295
297
  extra_headers
296
298
  end
297
299
 
298
- def headline_uri(request)
299
- request.path
300
- end
301
-
302
300
  def handle(request)
303
301
  catch(:buffer_full) do
304
302
  request.transition(:headers)
@@ -312,9 +310,14 @@ module HTTPX
312
310
  end
313
311
  end
314
312
 
313
+ def join_headline(request)
314
+ "#{request.verb.to_s.upcase} #{request.path} HTTP/#{@version.join(".")}"
315
+ end
316
+
315
317
  def join_headers(request)
316
- @buffer << "#{request.verb.to_s.upcase} #{headline_uri(request)} HTTP/#{@version.join(".")}" << CRLF
317
- log(color: :yellow) { "<- HEADLINE: #{@buffer.to_s.chomp.inspect}" }
318
+ headline = join_headline(request)
319
+ @buffer << headline << CRLF
320
+ log(color: :yellow) { "<- HEADLINE: #{headline.chomp.inspect}" }
318
321
  extra_headers = set_protocol_headers(request)
319
322
  join_headers2(request.headers.each(extra_headers))
320
323
  log { "<- " }
@@ -16,6 +16,12 @@ module HTTPX
16
16
  end
17
17
  end
18
18
 
19
+ class GoawayError < Error
20
+ def initialize
21
+ super(0, :no_error)
22
+ end
23
+ end
24
+
19
25
  attr_reader :streams, :pending
20
26
 
21
27
  def initialize(buffer, options)
@@ -152,10 +158,6 @@ module HTTPX
152
158
  end
153
159
  end
154
160
 
155
- def headline_uri(request)
156
- request.path
157
- end
158
-
159
161
  def handle(request, stream)
160
162
  catch(:buffer_full) do
161
163
  request.transition(:headers)
@@ -207,13 +209,19 @@ module HTTPX
207
209
  {
208
210
  ":scheme" => request.scheme,
209
211
  ":method" => request.verb.to_s.upcase,
210
- ":path" => headline_uri(request),
212
+ ":path" => request.path,
211
213
  ":authority" => request.authority,
212
214
  }
213
215
  end
214
216
 
215
217
  def join_headers(stream, request)
216
218
  extra_headers = set_protocol_headers(request)
219
+
220
+ if request.headers.key?("host")
221
+ log { "forbidden \"host\" header found (#{request.headers["host"]}), will use it as authority..." }
222
+ extra_headers[":authority"] = request.headers["host"]
223
+ end
224
+
217
225
  log(level: 1, color: :yellow) do
218
226
  request.headers.merge(extra_headers).each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
219
227
  end
@@ -302,7 +310,7 @@ module HTTPX
302
310
  @drains.delete(request)
303
311
  @streams.delete(request)
304
312
 
305
- if error && error != :no_error
313
+ if error
306
314
  ex = Error.new(stream.id, error)
307
315
  ex.set_backtrace(caller)
308
316
  response = ErrorResponse.new(request, ex, request.options)
@@ -344,9 +352,16 @@ module HTTPX
344
352
 
345
353
  def on_close(_last_frame, error, _payload)
346
354
  is_connection_closed = @connection.state == :closed
347
- if error && error != :no_error
355
+ if error
348
356
  @buffer.clear if is_connection_closed
349
- ex = Error.new(0, error)
357
+ if error == :no_error
358
+ ex = GoawayError.new
359
+ @pending.unshift(*@streams.keys)
360
+ @drains.clear
361
+ @streams.clear
362
+ else
363
+ ex = Error.new(0, error)
364
+ end
350
365
  ex.set_backtrace(caller)
351
366
  handle_error(ex)
352
367
  end