httpx 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_13_0.md +58 -0
  3. data/lib/httpx/chainable.rb +2 -2
  4. data/lib/httpx/connection.rb +17 -13
  5. data/lib/httpx/connection/http1.rb +4 -2
  6. data/lib/httpx/connection/http2.rb +1 -1
  7. data/lib/httpx/io/ssl.rb +30 -17
  8. data/lib/httpx/io/tcp.rb +45 -26
  9. data/lib/httpx/io/unix.rb +27 -12
  10. data/lib/httpx/options.rb +11 -23
  11. data/lib/httpx/plugins/compression.rb +20 -8
  12. data/lib/httpx/plugins/compression/brotli.rb +8 -6
  13. data/lib/httpx/plugins/compression/deflate.rb +2 -2
  14. data/lib/httpx/plugins/compression/gzip.rb +2 -2
  15. data/lib/httpx/plugins/digest_authentication.rb +1 -1
  16. data/lib/httpx/plugins/follow_redirects.rb +1 -1
  17. data/lib/httpx/plugins/h2c.rb +43 -58
  18. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  19. data/lib/httpx/plugins/retries.rb +1 -1
  20. data/lib/httpx/plugins/stream.rb +3 -1
  21. data/lib/httpx/plugins/upgrade.rb +83 -0
  22. data/lib/httpx/plugins/upgrade/h2.rb +54 -0
  23. data/lib/httpx/pool.rb +14 -5
  24. data/lib/httpx/response.rb +5 -5
  25. data/lib/httpx/version.rb +1 -1
  26. data/sig/chainable.rbs +2 -1
  27. data/sig/connection/http1.rbs +1 -0
  28. data/sig/options.rbs +7 -20
  29. data/sig/plugins/aws_sigv4.rbs +0 -1
  30. data/sig/plugins/compression.rbs +5 -3
  31. data/sig/plugins/compression/brotli.rbs +1 -1
  32. data/sig/plugins/compression/deflate.rbs +1 -1
  33. data/sig/plugins/compression/gzip.rbs +1 -1
  34. data/sig/plugins/cookies.rbs +0 -1
  35. data/sig/plugins/digest_authentication.rbs +0 -1
  36. data/sig/plugins/expect.rbs +0 -2
  37. data/sig/plugins/follow_redirects.rbs +0 -2
  38. data/sig/plugins/h2c.rbs +5 -10
  39. data/sig/plugins/persistent.rbs +0 -1
  40. data/sig/plugins/proxy.rbs +0 -1
  41. data/sig/plugins/retries.rbs +0 -4
  42. data/sig/plugins/upgrade.rbs +23 -0
  43. data/sig/response.rbs +3 -1
  44. metadata +7 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5770fadc8d4604ccb0fb274c7fc157802315f68c749a83973ee5596fee8effb1
4
- data.tar.gz: 4f24cca053093be31636405dcb740f5c6b2599197d9ebdfb90cbf127e86a5ffc
3
+ metadata.gz: ecaac1dac4ce953ba1585a418432d9bf635aea581a6d81acb32778ff947f486b
4
+ data.tar.gz: 32d9c5f65bd18ceeab524c6734081740ea2a3cbe526d3a6a71e079846f047644
5
5
  SHA512:
6
- metadata.gz: 48fe64207a82e2b8db91b73cc6252ff48ea624e57dcbcbfa30e5f6986e1936e61bbdc83cece34c470dd6e318f41845a306a57e2b5c8710084444b6193786a1eb
7
- data.tar.gz: 70b43fc624a8452187a730e39f55d9e92d16438babbd76aa2346c5b3dd0c2cad7f8e5e787f7c4ab1917c52ed750c7656f2f6c5e92a2ec0fce9cc0a76e5994d1e
6
+ metadata.gz: 204b0aea0e3bc09eccd31c9b93c79de866cff737801bca75096a61ea3ff45e62d44b2e9d39e26e3fb34d261dc518e17766080549f55b1cf0e16cfcd55565ddb4
7
+ data.tar.gz: ffc06e694b6afd9e29d48857c0251b75829d41637583f45243dd7b30931f0b9a4f8bf06d13421e94dc5f07bf5113ff0f9f2c888be6313c9506e6da05659392a1
@@ -0,0 +1,58 @@
1
+ # 0.13.0
2
+
3
+ ## Features
4
+
5
+ ### Upgrade plugin
6
+
7
+ A new plugin, `:upgrade`, is now available. This plugin allows one to "hook" on HTTP/1.1's protocol upgrade mechanism (see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism), which is the mechanism that browsers use to initiate websockets (there is an example of how to use `httpx` to start a websocket client connection [in the tests](https://gitlab.com/honeyryderchuck/httpx/-/blob/master/test/support/requests/plugins/upgrade.rb))
8
+
9
+ You can read more about the `:upgrade` plugin in the [wiki](https://honeyryderchuck.gitlab.io/httpx/wiki/Connection-Upgrade).
10
+
11
+ It's the basis of two plugins:
12
+
13
+ #### `:h2c`
14
+
15
+ This plugin was been rewritten on top of the `:upgrade` plugin, and handles upgrading a plaintext (non-"https") HTTP/1.1 connection, into an HTTP/2 connection.
16
+
17
+ https://honeyryderchuck.gitlab.io/httpx/wiki/Connection-Upgrade#h2c
18
+
19
+ #### `:upgrade/h2`
20
+
21
+ This plugin handles when a server responds to a request with an `Upgrade: h2` header, does the following requests to the same origin via HTTP/2 prior knowledge (bypassing the necessity for ALPN negotiation, which is the whole point of the feature).
22
+
23
+ https://honeyryderchuck.gitlab.io/httpx/wiki/Connection-Upgrade#h2
24
+
25
+ ### `:addresses` option
26
+
27
+ The `:addresses` option is now available. You can use it to pass a list of IPs to connect to:
28
+
29
+ ```ruby
30
+ # will not resolve example.com, and instead connect to one of the IPs passed.
31
+ HTTPX.get("http://example.com", addresses: %w[172.5.3.1 172.5.3.2]))
32
+ ```
33
+
34
+ You should also use it to connect to HTTP servers bound to a UNIX socket, in which case you'll have to provide a path:
35
+
36
+ ```ruby
37
+ HTTPX.get("http://example.com", addresses: %w[/path/to/usocket]))
38
+ ```
39
+
40
+ The `:transport_options` are therefore deprecated, and will be moved in a major version.
41
+
42
+ ## Improvements
43
+
44
+ Some internal improvements that allow certainn plugins not to "leak" globally, such as the `:compression` plugin, which used to enable compression for all the `httpx` sessions from the same process. It doesn't anymore.
45
+
46
+ Using exceptionless nonblocking connect calls in the supported rubies.
47
+
48
+ Removed unneeded APIs around the Options object (`with_` methods, or the defined options list).
49
+
50
+ ## Bugfixes
51
+
52
+ HTTP/1.1 persistent connections were closing after each request after the max requests was reached. It's fixed, and the new connection will also be persistent.
53
+
54
+ When passing open IO objects for origins (the `:io` option), `httpx` was still trying to resolve the origin's domain. This not only didn't make sense, it broke if the domain is unresolvable. It has been fixed.
55
+
56
+ Fixed usage of `:io` option when passed an "authority/io" hash.
57
+
58
+ Fixing some issues around trying to connnect to the next available IPAddress when the previous one was unreachable or ETIMEDOUT.
@@ -17,12 +17,12 @@ module HTTPX
17
17
  # :nocov:
18
18
  def timeout(**args)
19
19
  warn ":#{__method__} is deprecated, use :with_timeout instead"
20
- branch(default_options.with(timeout: args))
20
+ with(timeout: args)
21
21
  end
22
22
 
23
23
  def headers(headers)
24
24
  warn ":#{__method__} is deprecated, use :with_headers instead"
25
- branch(default_options.with(headers: headers))
25
+ with(headers: headers)
26
26
  end
27
27
  # :nocov:
28
28
 
@@ -71,6 +71,8 @@ module HTTPX
71
71
  @inflight = 0
72
72
  @keep_alive_timeout = options.timeout.keep_alive_timeout
73
73
  @keep_alive_timer = nil
74
+
75
+ self.addresses = options.addresses if options.addresses
74
76
  end
75
77
 
76
78
  # this is a semi-private method, to be used by the resolver
@@ -105,6 +107,8 @@ module HTTPX
105
107
 
106
108
  return false if exhausted?
107
109
 
110
+ return false unless connection.addresses
111
+
108
112
  !(@io.addresses & connection.addresses).empty? && @options == connection.options
109
113
  end
110
114
 
@@ -472,26 +476,15 @@ module HTTPX
472
476
  remove_instance_variable(:@total_timeout)
473
477
  end
474
478
 
475
- @io.close if @io
476
- @read_buffer.clear
477
- if @keep_alive_timer
478
- @keep_alive_timer.cancel
479
- remove_instance_variable(:@keep_alive_timer)
480
- end
481
-
482
- remove_instance_variable(:@timeout) if defined?(@timeout)
479
+ purge_after_closed
483
480
  when :already_open
484
481
  nextstate = :open
485
482
  send_pending
486
483
  end
487
484
  @state = nextstate
488
- rescue Errno::EHOSTUNREACH
489
- # at this point, all addresses from the IO object have failed
490
- reset
491
- emit(:unreachable)
492
- throw(:jump_tick)
493
485
  rescue Errno::ECONNREFUSED,
494
486
  Errno::EADDRNOTAVAIL,
487
+ Errno::EHOSTUNREACH,
495
488
  TLSError => e
496
489
  # connect errors, exit gracefully
497
490
  handle_error(e)
@@ -499,6 +492,17 @@ module HTTPX
499
492
  emit(:close)
500
493
  end
501
494
 
495
+ def purge_after_closed
496
+ @io.close if @io
497
+ @read_buffer.clear
498
+ if @keep_alive_timer
499
+ @keep_alive_timer.cancel
500
+ remove_instance_variable(:@keep_alive_timer)
501
+ end
502
+
503
+ remove_instance_variable(:@timeout) if defined?(@timeout)
504
+ end
505
+
502
506
  def handle_response
503
507
  @inflight -= 1
504
508
  return unless @inflight.zero?
@@ -10,7 +10,7 @@ module HTTPX
10
10
  MAX_REQUESTS = 100
11
11
  CRLF = "\r\n"
12
12
 
13
- attr_reader :pending
13
+ attr_reader :pending, :requests
14
14
 
15
15
  def initialize(buffer, options)
16
16
  @options = Options.new(options)
@@ -69,7 +69,6 @@ module HTTPX
69
69
 
70
70
  return if @requests.include?(request)
71
71
 
72
- request.once(:headers, &method(:set_protocol_headers))
73
72
  @requests << request
74
73
  @pipelining = true if @requests.size > 1
75
74
  end
@@ -236,6 +235,8 @@ module HTTPX
236
235
 
237
236
  def disable_pipelining
238
237
  return if @requests.empty?
238
+ # do not disable pipelining if already set to 1 request at a time
239
+ return if @max_concurrent_requests == 1
239
240
 
240
241
  @requests.each do |r|
241
242
  r.transition(:idle)
@@ -281,6 +282,7 @@ module HTTPX
281
282
  log(color: :yellow) { "<- HEADLINE: #{buffer.chomp.inspect}" }
282
283
  @buffer << buffer
283
284
  buffer.clear
285
+ set_protocol_headers(request)
284
286
  request.headers.each do |field, value|
285
287
  buffer << "#{capitalized(field)}: #{value}" << CRLF
286
288
  log(color: :yellow) { "<- HEADER: #{buffer.chomp}" }
@@ -91,7 +91,6 @@ module HTTPX
91
91
  @streams[request] = stream
92
92
  @max_requests -= 1
93
93
  end
94
- request.once(:headers, &method(:set_protocol_headers))
95
94
  handle(request, stream)
96
95
  true
97
96
  rescue HTTP2Next::Error::StreamLimitExceeded
@@ -187,6 +186,7 @@ module HTTPX
187
186
  end
188
187
 
189
188
  def join_headers(stream, request)
189
+ set_protocol_headers(request)
190
190
  log(level: 1, color: :yellow) do
191
191
  request.headers.each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
192
192
  end
data/lib/httpx/io/ssl.rb CHANGED
@@ -47,10 +47,6 @@ module HTTPX
47
47
 
48
48
  def connect
49
49
  super
50
- if @keep_open
51
- @state = :negotiated
52
- return
53
- end
54
50
  return if @state == :negotiated ||
55
51
  @state != :connected
56
52
 
@@ -59,17 +55,22 @@ module HTTPX
59
55
  @io.hostname = @sni_hostname
60
56
  @io.sync_close = true
61
57
  end
62
- @io.connect_nonblock
63
- @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
64
- transition(:negotiated)
65
- @interests = :w
66
- rescue ::IO::WaitReadable
67
- @interests = :r
68
- rescue ::IO::WaitWritable
69
- @interests = :w
58
+ try_ssl_connect
70
59
  end
71
60
 
72
61
  if RUBY_VERSION < "2.3"
62
+ # :nocov:
63
+ def try_ssl_connect
64
+ @io.connect_nonblock
65
+ @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
66
+ transition(:negotiated)
67
+ @interests = :w
68
+ rescue ::IO::WaitReadable
69
+ @interests = :r
70
+ rescue ::IO::WaitWritable
71
+ @interests = :w
72
+ end
73
+
73
74
  def read(_, buffer)
74
75
  super
75
76
  rescue ::IO::WaitWritable
@@ -82,7 +83,23 @@ module HTTPX
82
83
  rescue ::IO::WaitReadable
83
84
  0
84
85
  end
86
+ # :nocov:
85
87
  else
88
+ def try_ssl_connect
89
+ case @io.connect_nonblock(exception: false)
90
+ when :wait_readable
91
+ @interests = :r
92
+ return
93
+ when :wait_writable
94
+ @interests = :w
95
+ return
96
+ end
97
+ @io.post_connection_check(@sni_hostname) if @ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
98
+ transition(:negotiated)
99
+ @interests = :w
100
+ end
101
+
102
+ # :nocov:
86
103
  if OpenSSL::VERSION < "2.0.6"
87
104
  def read(size, buffer)
88
105
  @io.read_nonblock(size, buffer)
@@ -95,11 +112,7 @@ module HTTPX
95
112
  nil
96
113
  end
97
114
  end
98
- end
99
-
100
- def inspect
101
- id = @io.closed? ? "closed" : @io.to_io.fileno
102
- "#<SSL(fd: #{id}): #{@ip}:#{@port} state: #{@state}>"
115
+ # :nocov:
103
116
  end
104
117
 
105
118
  private
data/lib/httpx/io/tcp.rb CHANGED
@@ -7,6 +7,8 @@ module HTTPX
7
7
  class TCP
8
8
  include Loggable
9
9
 
10
+ using URIExtensions
11
+
10
12
  attr_reader :ip, :port, :addresses, :state, :interests
11
13
 
12
14
  alias_method :host, :ip
@@ -14,7 +16,6 @@ module HTTPX
14
16
  def initialize(origin, addresses, options)
15
17
  @state = :idle
16
18
  @hostname = origin.host
17
- @addresses = addresses
18
19
  @options = Options.new(options)
19
20
  @fallback_protocol = @options.fallback_protocol
20
21
  @port = origin.port
@@ -26,17 +27,17 @@ module HTTPX
26
27
  else
27
28
  @options.io
28
29
  end
30
+ raise Error, "Given IO objects do not match the request authority" unless @io
31
+
29
32
  _, _, _, @ip = @io.addr
30
33
  @addresses ||= [@ip]
31
34
  @ip_index = @addresses.size - 1
32
- unless @io.nil?
33
- @keep_open = true
34
- @state = :connected
35
- end
35
+ @keep_open = true
36
+ @state = :connected
36
37
  else
37
- @ip_index = @addresses.size - 1
38
- @ip = @addresses[@ip_index]
38
+ @addresses = addresses.map { |addr| addr.is_a?(IPAddr) ? addr : IPAddr.new(addr) }
39
39
  end
40
+ @ip_index = @addresses.size - 1
40
41
  @io ||= build_socket
41
42
  end
42
43
 
@@ -51,17 +52,11 @@ module HTTPX
51
52
  def connect
52
53
  return unless closed?
53
54
 
54
- begin
55
- if @io.closed?
56
- transition(:idle)
57
- @io = build_socket
58
- end
59
- @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s))
60
- rescue Errno::EISCONN
55
+ if @io.closed?
56
+ transition(:idle)
57
+ @io = build_socket
61
58
  end
62
- @interests = :w
63
-
64
- transition(:connected)
59
+ try_connect
65
60
  rescue Errno::EHOSTUNREACH => e
66
61
  raise e if @ip_index <= 0
67
62
 
@@ -72,15 +67,25 @@ module HTTPX
72
67
 
73
68
  @ip_index -= 1
74
69
  retry
75
- rescue Errno::EINPROGRESS,
76
- Errno::EALREADY
77
- @interests = :w
78
- rescue ::IO::WaitReadable
79
- @interests = :r
80
70
  end
81
71
 
82
72
  if RUBY_VERSION < "2.3"
83
73
  # :nocov:
74
+ def try_connect
75
+ @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s))
76
+ rescue ::IO::WaitWritable, Errno::EALREADY
77
+ @interests = :w
78
+ rescue ::IO::WaitReadable
79
+ @interests = :r
80
+ rescue Errno::EISCONN
81
+ transition(:connected)
82
+ @interests = :w
83
+ else
84
+ transition(:connected)
85
+ @interests = :w
86
+ end
87
+ private :try_connect
88
+
84
89
  def read(size, buffer)
85
90
  @io.read_nonblock(size, buffer)
86
91
  log { "READ: #{buffer.bytesize} bytes..." }
@@ -104,6 +109,20 @@ module HTTPX
104
109
  end
105
110
  # :nocov:
106
111
  else
112
+ def try_connect
113
+ case @io.connect_nonblock(Socket.sockaddr_in(@port, @ip.to_s), exception: false)
114
+ when :wait_readable
115
+ @interests = :r
116
+ return
117
+ when :wait_writable
118
+ @interests = :w
119
+ return
120
+ end
121
+ transition(:connected)
122
+ @interests = :w
123
+ end
124
+ private :try_connect
125
+
107
126
  def read(size, buffer)
108
127
  ret = @io.read_nonblock(size, buffer, exception: false)
109
128
  if ret == :wait_readable
@@ -148,14 +167,14 @@ module HTTPX
148
167
 
149
168
  # :nocov:
150
169
  def inspect
151
- id = @io.closed? ? "closed" : @io.fileno
152
- "#<TCP(fd: #{id}): #{@ip}:#{@port} (state: #{@state})>"
170
+ "#<#{self.class}: #{@ip}:#{@port} (state: #{@state})>"
153
171
  end
154
172
  # :nocov:
155
173
 
156
174
  private
157
175
 
158
176
  def build_socket
177
+ @ip = @addresses[@ip_index]
159
178
  Socket.new(@ip.family, :STREAM, 0)
160
179
  end
161
180
 
@@ -178,9 +197,9 @@ module HTTPX
178
197
  def log_transition_state(nextstate)
179
198
  case nextstate
180
199
  when :connected
181
- "Connected to #{@hostname} (#{@ip}) port #{@port} (##{@io.fileno})"
200
+ "Connected to #{host} (##{@io.fileno})"
182
201
  else
183
- "#{@ip}:#{@port} #{@state} -> #{nextstate}"
202
+ "#{host} #{@state} -> #{nextstate}"
184
203
  end
185
204
  end
186
205
  end
data/lib/httpx/io/unix.rb CHANGED
@@ -6,34 +6,43 @@ module HTTPX
6
6
  class UNIX < TCP
7
7
  extend Forwardable
8
8
 
9
- def_delegator :@uri, :port, :scheme
9
+ using URIExtensions
10
10
 
11
- def initialize(uri, addresses, options)
12
- @uri = uri
11
+ attr_reader :path
12
+
13
+ alias_method :host, :path
14
+
15
+ def initialize(origin, addresses, options)
13
16
  @addresses = addresses
17
+ @hostname = origin.host
14
18
  @state = :idle
15
19
  @options = Options.new(options)
16
- @path = @options.transport_options[:path]
17
20
  @fallback_protocol = @options.fallback_protocol
18
21
  if @options.io
19
22
  @io = case @options.io
20
23
  when Hash
21
- @options.io[@path]
24
+ @options.io[origin.authority]
22
25
  else
23
26
  @options.io
24
27
  end
25
- unless @io.nil?
26
- @keep_open = true
27
- @state = :connected
28
+ raise Error, "Given IO objects do not match the request authority" unless @io
29
+
30
+ @path = @io.path
31
+ @keep_open = true
32
+ @state = :connected
33
+ else
34
+ if @options.transport_options
35
+ # :nocov:
36
+ warn ":#{__method__} is deprecated, use :addresses instead"
37
+ @path = @options.transport_options[:path]
38
+ # :nocov:
39
+ else
40
+ @path = addresses.first
28
41
  end
29
42
  end
30
43
  @io ||= build_socket
31
44
  end
32
45
 
33
- def hostname
34
- @uri.host
35
- end
36
-
37
46
  def connect
38
47
  return unless closed?
39
48
 
@@ -51,6 +60,12 @@ module HTTPX
51
60
  ::IO::WaitReadable
52
61
  end
53
62
 
63
+ # :nocov:
64
+ def inspect
65
+ "#<#{self.class}(path: #{@path}): (state: #{@state})>"
66
+ end
67
+ # :nocov:
68
+
54
69
  private
55
70
 
56
71
  def build_socket