httpx 1.1.4 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/doc/release_notes/1_1_4.md +1 -1
  4. data/doc/release_notes/1_1_5.md +12 -0
  5. data/doc/release_notes/1_2_0.md +49 -0
  6. data/lib/httpx/adapters/webmock.rb +29 -8
  7. data/lib/httpx/altsvc.rb +57 -2
  8. data/lib/httpx/chainable.rb +40 -29
  9. data/lib/httpx/connection/http1.rb +27 -22
  10. data/lib/httpx/connection/http2.rb +7 -3
  11. data/lib/httpx/connection.rb +45 -60
  12. data/lib/httpx/extensions.rb +0 -15
  13. data/lib/httpx/options.rb +84 -27
  14. data/lib/httpx/plugins/aws_sigv4.rb +2 -2
  15. data/lib/httpx/plugins/basic_auth.rb +1 -1
  16. data/lib/httpx/plugins/callbacks.rb +91 -0
  17. data/lib/httpx/plugins/circuit_breaker.rb +2 -0
  18. data/lib/httpx/plugins/cookies.rb +19 -9
  19. data/lib/httpx/plugins/digest_auth.rb +1 -1
  20. data/lib/httpx/plugins/follow_redirects.rb +11 -0
  21. data/lib/httpx/plugins/grpc/call.rb +2 -3
  22. data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -0
  23. data/lib/httpx/plugins/grpc.rb +2 -2
  24. data/lib/httpx/plugins/h2c.rb +20 -8
  25. data/lib/httpx/plugins/proxy/socks4.rb +2 -2
  26. data/lib/httpx/plugins/proxy/socks5.rb +2 -2
  27. data/lib/httpx/plugins/proxy.rb +14 -32
  28. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  29. data/lib/httpx/plugins/retries.rb +4 -0
  30. data/lib/httpx/plugins/ssrf_filter.rb +142 -0
  31. data/lib/httpx/plugins/stream.rb +9 -4
  32. data/lib/httpx/plugins/upgrade/h2.rb +1 -1
  33. data/lib/httpx/plugins/upgrade.rb +1 -1
  34. data/lib/httpx/plugins/webdav.rb +1 -1
  35. data/lib/httpx/pool.rb +32 -28
  36. data/lib/httpx/request/body.rb +3 -3
  37. data/lib/httpx/request.rb +12 -3
  38. data/lib/httpx/resolver/https.rb +3 -2
  39. data/lib/httpx/resolver/native.rb +1 -0
  40. data/lib/httpx/resolver/resolver.rb +17 -6
  41. data/lib/httpx/response.rb +1 -1
  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/plugins/stream.rbs +24 -22
  58. data/sig/pool.rbs +1 -3
  59. data/sig/resolver/resolver.rbs +3 -1
  60. data/sig/session.rbs +4 -4
  61. metadata +12 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8999f3068b60027a01d29f4d840ff77828a151eaf377bec3959c26fcc0b2e25
4
- data.tar.gz: cb1490afe5522a3c77c0a1f5ef44267d60d7e23993718c58f18525e971bf70d9
3
+ metadata.gz: f3df88a59256b85aacf9d98578c965626b3bfa4611695ae21a707252df71cb0a
4
+ data.tar.gz: f1616869724c217272d0dc165130548edc8ca35eadd97ab42d5a17012a0019d6
5
5
  SHA512:
6
- metadata.gz: 5d29a339eb2781a8d8c1ff9b3c95f03717034eb32210639e355f8687129d80b614290d699d00e6bd5781998f14564a8dec4e1750545841e30dd53eaba58d3a9c
7
- data.tar.gz: d9e0dd9966132648aa4ded10fe71cb94496337328575d0c99d6b861536401820c354742d5a923d79e2a7eff77a8649e7f1630e7c895e6f14fdbcf6e5e48bd828
6
+ metadata.gz: b4ca8efc0a1ffe3dec2d2d6ee345bcb58bc2eec9e4c5b4fe951a889041be8eeaeb4b005d9771f0e712f161dad69f8de25d07d520c3e0cd17ae1574f53fadace1
7
+ data.tar.gz: 509c48d74aafb520e3142c5f9b2356e2f36bf9d649eb723ca07a2863077057ab12ae2e17dbf61f13149eac4f80201252dad9b46cb904f5664b8910b62a868c1d
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
 
@@ -1,6 +1,6 @@
1
1
  # 1.1.4
2
2
 
3
- ## bug reports
3
+ ## bugfixes
4
4
 
5
5
  * datadog adapter: use `Gem::Version` to invoke the correct configuration API.
6
6
  * stream plugin: do not preempt request enqueuing (this was making integration with the `:follow_redirects` plugin fail when set up with `webmock`).
@@ -0,0 +1,12 @@
1
+ # 1.1.5
2
+
3
+ ## improvements
4
+
5
+ * pattern matching support for responses has been backported to ruby 2.7 as well.
6
+
7
+ ## bugfixes
8
+
9
+ * `stream` plugin: fix for `HTTPX::StreamResponse#each_line` not yielding the last line of the payload when not delimiter-terminated.
10
+ * `stream` plugin: fix `webmock` adapter integration when methods calls would happen in the `HTTPX::StreamResponse#each` block.
11
+ * `stream` plugin: fix `:follow_redirects` plugin integration which was caching the redirect response and using it for method calls inside the `HTTPX::StreamResponse#each` block.
12
+ * "103 early hints" responses will be ignored when processing the response (it was causing the response returned by sesssions to hold its headers, instead of the following 200 response, while keeping the 200 response body).
@@ -0,0 +1,49 @@
1
+ # 1.2.0
2
+
3
+ ## Features
4
+
5
+ ### `:ssrf_filter` plugin
6
+
7
+ The `:ssrf_filter` p 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.
@@ -38,12 +38,12 @@ module WebMock
38
38
 
39
39
  return build_error_response(request, webmock_response.exception) if webmock_response.exception
40
40
 
41
- response = request.options.response_class.new(request,
42
- webmock_response.status[0],
43
- "2.0",
44
- webmock_response.headers)
45
- response << webmock_response.body.dup
46
- response
41
+ request.options.response_class.new(request,
42
+ webmock_response.status[0],
43
+ "2.0",
44
+ webmock_response.headers).tap do |res|
45
+ res.mocked = true
46
+ end
47
47
  end
48
48
 
49
49
  def build_error_response(request, exception)
@@ -52,16 +52,36 @@ module WebMock
52
52
  end
53
53
 
54
54
  module InstanceMethods
55
- def build_connection(*)
55
+ def init_connection(*)
56
56
  connection = super
57
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
58
62
  pool.__send__(:resolve_connection, connection)
59
- pool.__send__(:unregister_connection, connection) unless connection.addresses
60
63
  end
61
64
  connection
62
65
  end
63
66
  end
64
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
+
65
85
  module ConnectionMethods
66
86
  def initialize(*)
67
87
  super
@@ -90,6 +110,7 @@ module WebMock
90
110
  log { "mocking #{request.uri} with #{mock_response.inspect}" }
91
111
  request.response = response
92
112
  request.emit(:response, response)
113
+ response << mock_response.body.dup unless response.is_a?(HTTPX::ErrorResponse)
93
114
  elsif WebMock.net_connect_allowed?(request_signature.uri)
94
115
  if WebMock::CallbackRegistry.any_callbacks?
95
116
  request.on(:response) do |resp|
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}")
@@ -10,19 +10,6 @@ module HTTPX
10
10
  MOD
11
11
  end
12
12
 
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
-
26
13
  def request(*args, **options)
27
14
  branch(default_options).request(*args, **options)
28
15
  end
@@ -46,12 +33,6 @@ module HTTPX
46
33
  branch(default_options.merge(options), &blk)
47
34
  end
48
35
 
49
- protected
50
-
51
- def on(*args, &blk)
52
- branch(default_options).on(*args, &blk)
53
- end
54
-
55
36
  private
56
37
 
57
38
  def default_options
@@ -64,22 +45,52 @@ module HTTPX
64
45
  Session.new(options, &blk)
65
46
  end
66
47
 
67
- def method_missing(meth, *args, **options)
68
- return super unless meth =~ /\Awith_(.+)/
48
+ def method_missing(meth, *args, **options, &blk)
49
+ case meth
50
+ when /\Awith_(.+)/
69
51
 
70
- option = Regexp.last_match(1)
52
+ option = Regexp.last_match(1)
71
53
 
72
- return super unless option
54
+ return super unless option
73
55
 
74
- with(option.to_sym => (args.first || options))
75
- end
56
+ with(option.to_sym => args.first || options)
57
+ when /\Aon_(.+)/
58
+ callback = Regexp.last_match(1)
76
59
 
77
- def respond_to_missing?(meth, *)
78
- return super unless meth =~ /\Awith_(.+)/
60
+ return super unless %w[
61
+ connection_opened connection_closed
62
+ request_error
63
+ request_started request_body_chunk request_completed
64
+ response_started response_body_chunk response_completed
65
+ ].include?(callback)
79
66
 
80
- option = Regexp.last_match(1)
67
+ warn "DEPRECATION WARNING: calling `.#{meth}` on plain HTTPX sessions is deprecated. " \
68
+ "Use HTTPX.plugin(:callbacks).#{meth} instead."
81
69
 
82
- default_options.respond_to?(option) || super
70
+ plugin(:callbacks).__send__(meth, *args, **options, &blk)
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+ def respond_to_missing?(meth, *)
77
+ case meth
78
+ when /\Awith_(.+)/
79
+ option = Regexp.last_match(1)
80
+
81
+ default_options.respond_to?(option) || super
82
+ when /\Aon_(.+)/
83
+ callback = Regexp.last_match(1)
84
+
85
+ %w[
86
+ connection_opened connection_closed
87
+ request_error
88
+ request_started request_body_chunk request_completed
89
+ response_started response_body_chunk response_completed
90
+ ].include?(callback) || super
91
+ else
92
+ super
93
+ end
83
94
  end
84
95
  end
85
96
  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