httpx 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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