httpx 0.12.0 → 0.14.1

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_10_1.md +1 -1
  3. data/doc/release_notes/0_13_0.md +58 -0
  4. data/doc/release_notes/0_13_1.md +5 -0
  5. data/doc/release_notes/0_13_2.md +9 -0
  6. data/doc/release_notes/0_14_0.md +79 -0
  7. data/doc/release_notes/0_14_1.md +7 -0
  8. data/lib/httpx.rb +1 -2
  9. data/lib/httpx/callbacks.rb +12 -3
  10. data/lib/httpx/chainable.rb +2 -2
  11. data/lib/httpx/connection.rb +29 -22
  12. data/lib/httpx/connection/http1.rb +35 -15
  13. data/lib/httpx/connection/http2.rb +61 -15
  14. data/lib/httpx/headers.rb +7 -3
  15. data/lib/httpx/io/ssl.rb +30 -17
  16. data/lib/httpx/io/tcp.rb +48 -27
  17. data/lib/httpx/io/udp.rb +31 -7
  18. data/lib/httpx/io/unix.rb +27 -12
  19. data/lib/httpx/options.rb +97 -74
  20. data/lib/httpx/plugins/aws_sdk_authentication.rb +5 -2
  21. data/lib/httpx/plugins/aws_sigv4.rb +5 -4
  22. data/lib/httpx/plugins/basic_authentication.rb +8 -3
  23. data/lib/httpx/plugins/compression.rb +24 -12
  24. data/lib/httpx/plugins/compression/brotli.rb +10 -7
  25. data/lib/httpx/plugins/compression/deflate.rb +6 -5
  26. data/lib/httpx/plugins/compression/gzip.rb +4 -3
  27. data/lib/httpx/plugins/cookies.rb +3 -7
  28. data/lib/httpx/plugins/digest_authentication.rb +5 -5
  29. data/lib/httpx/plugins/expect.rb +6 -6
  30. data/lib/httpx/plugins/follow_redirects.rb +4 -4
  31. data/lib/httpx/plugins/grpc.rb +247 -0
  32. data/lib/httpx/plugins/grpc/call.rb +62 -0
  33. data/lib/httpx/plugins/grpc/message.rb +85 -0
  34. data/lib/httpx/plugins/h2c.rb +43 -58
  35. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  36. data/lib/httpx/plugins/multipart/part.rb +2 -2
  37. data/lib/httpx/plugins/proxy.rb +3 -7
  38. data/lib/httpx/plugins/proxy/http.rb +5 -4
  39. data/lib/httpx/plugins/proxy/ssh.rb +3 -3
  40. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  41. data/lib/httpx/plugins/retries.rb +14 -15
  42. data/lib/httpx/plugins/stream.rb +99 -75
  43. data/lib/httpx/plugins/upgrade.rb +84 -0
  44. data/lib/httpx/plugins/upgrade/h2.rb +54 -0
  45. data/lib/httpx/pool.rb +14 -5
  46. data/lib/httpx/request.rb +25 -2
  47. data/lib/httpx/resolver/native.rb +7 -3
  48. data/lib/httpx/response.rb +9 -5
  49. data/lib/httpx/session.rb +17 -7
  50. data/lib/httpx/transcoder/chunker.rb +1 -1
  51. data/lib/httpx/version.rb +1 -1
  52. data/sig/callbacks.rbs +2 -0
  53. data/sig/chainable.rbs +2 -1
  54. data/sig/connection/http1.rbs +6 -1
  55. data/sig/connection/http2.rbs +6 -2
  56. data/sig/headers.rbs +2 -2
  57. data/sig/options.rbs +16 -22
  58. data/sig/plugins/aws_sdk_authentication.rbs +2 -0
  59. data/sig/plugins/aws_sigv4.rbs +0 -1
  60. data/sig/plugins/basic_authentication.rbs +2 -0
  61. data/sig/plugins/compression.rbs +7 -5
  62. data/sig/plugins/compression/brotli.rbs +1 -1
  63. data/sig/plugins/compression/deflate.rbs +1 -1
  64. data/sig/plugins/compression/gzip.rbs +1 -1
  65. data/sig/plugins/cookies.rbs +0 -1
  66. data/sig/plugins/digest_authentication.rbs +0 -1
  67. data/sig/plugins/expect.rbs +0 -2
  68. data/sig/plugins/follow_redirects.rbs +0 -2
  69. data/sig/plugins/h2c.rbs +5 -10
  70. data/sig/plugins/persistent.rbs +0 -1
  71. data/sig/plugins/proxy.rbs +0 -1
  72. data/sig/plugins/retries.rbs +0 -4
  73. data/sig/plugins/stream.rbs +17 -16
  74. data/sig/plugins/upgrade.rbs +23 -0
  75. data/sig/request.rbs +7 -2
  76. data/sig/response.rbs +4 -1
  77. data/sig/session.rbs +4 -0
  78. metadata +21 -7
  79. data/lib/httpx/timeout.rb +0 -67
  80. data/sig/timeout.rbs +0 -29
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin helps negotiating a new protocol from an HTTP/1.1 connection, via the
7
+ # Upgrade header.
8
+ #
9
+ # https://gitlab.com/honeyryderchuck/httpx/wikis/Upgrade
10
+ #
11
+ module Upgrade
12
+ class << self
13
+ def configure(klass)
14
+ klass.plugin(:"upgrade/h2")
15
+ end
16
+
17
+ def extra_options(options)
18
+ upgrade_handlers = Module.new do
19
+ extend Registry
20
+ end
21
+
22
+ Class.new(options.class) do
23
+ def_option(:upgrade_handlers, <<-OUT)
24
+ raise Error, ":upgrade_handlers must be a registry" unless value.respond_to?(:registry)
25
+
26
+ value
27
+ OUT
28
+ end.new(options).merge(upgrade_handlers: upgrade_handlers)
29
+ end
30
+ end
31
+
32
+ module InstanceMethods
33
+ def fetch_response(request, connections, options)
34
+ response = super
35
+
36
+ if response
37
+ return response unless response.respond_to?(:headers) && response.headers.key?("upgrade")
38
+
39
+ upgrade_protocol = response.headers["upgrade"].split(/ *, */).first
40
+
41
+ return response unless upgrade_protocol && options.upgrade_handlers.registry.key?(upgrade_protocol)
42
+
43
+ protocol_handler = options.upgrade_handlers.registry(upgrade_protocol)
44
+
45
+ return response unless protocol_handler
46
+
47
+ log { "upgrading to #{upgrade_protocol}..." }
48
+ connection = find_connection(request, connections, options)
49
+ connections << connection unless connections.include?(connection)
50
+
51
+ # do not upgrade already upgraded connections
52
+ return if connection.upgrade_protocol == upgrade_protocol
53
+
54
+ protocol_handler.call(connection, request, response)
55
+
56
+ # keep in the loop if the server is switching, unless
57
+ # the connection has been hijacked, in which case you want
58
+ # to terminante immediately
59
+ return if response.status == 101 && !connection.hijacked
60
+ end
61
+
62
+ response
63
+ end
64
+
65
+ def close(*args)
66
+ return super if args.empty?
67
+
68
+ connections, = args
69
+
70
+ pool.close(connections.reject(&:hijacked))
71
+ end
72
+ end
73
+
74
+ module ConnectionMethods
75
+ attr_reader :upgrade_protocol, :hijacked
76
+
77
+ def hijack_io
78
+ @hijacked = true
79
+ end
80
+ end
81
+ end
82
+ register_plugin(:upgrade, Upgrade)
83
+ end
84
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds support for upgrading an HTTP/1.1 connection to HTTP/2
7
+ # via an Upgrade: h2 response declaration
8
+ #
9
+ # https://gitlab.com/honeyryderchuck/httpx/wikis/Upgrade#h2
10
+ #
11
+ module H2
12
+ class << self
13
+ def configure(klass)
14
+ klass.default_options.upgrade_handlers.register "h2", self
15
+ end
16
+
17
+ def call(connection, _request, _response)
18
+ connection.upgrade_to_h2
19
+ end
20
+ end
21
+
22
+ module ConnectionMethods
23
+ using URIExtensions
24
+
25
+ def upgrade_to_h2
26
+ prev_parser = @parser
27
+
28
+ if prev_parser
29
+ prev_parser.reset
30
+ @inflight -= prev_parser.requests.size
31
+ end
32
+
33
+ @parser = Connection::HTTP2.new(@write_buffer, @options)
34
+ set_parser_callbacks(@parser)
35
+ @upgrade_protocol = :h2
36
+
37
+ # what's happening here:
38
+ # a deviation from the state machine is done to perform the actions when a
39
+ # connection is closed, without transitioning, so the connection is kept in the pool.
40
+ # the state is reset to initial, so that the socket reconnect works out of the box,
41
+ # while the parser is already here.
42
+ purge_after_closed
43
+ transition(:idle)
44
+
45
+ prev_parser.requests.each do |req|
46
+ req.transition(:idle)
47
+ send(req)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ register_plugin(:"upgrade/h2", H2)
53
+ end
54
+ end
data/lib/httpx/pool.rb CHANGED
@@ -64,11 +64,6 @@ module HTTPX
64
64
  connection.on(:open) do
65
65
  @connected_connections += 1
66
66
  end
67
- connection.on(:unreachable) do
68
- resolver = find_resolver_for(connection)
69
- resolver.uncache(connection) if resolver
70
- resolve_connection(connection)
71
- end
72
67
  end
73
68
 
74
69
  # opens a connection to the IP reachable through +uri+.
@@ -85,6 +80,20 @@ module HTTPX
85
80
 
86
81
  def resolve_connection(connection)
87
82
  @connections << connection unless @connections.include?(connection)
83
+
84
+ if connection.addresses || connection.state == :open
85
+ #
86
+ # there are two cases in which we want to activate initialization of
87
+ # connection immediately:
88
+ #
89
+ # 1. when the connection already has addresses, i.e. it doesn't need to
90
+ # resolve a name (not the same as name being an IP, yet)
91
+ # 2. when the connection is initialized with an external already open IO.
92
+ #
93
+ on_resolver_connection(connection)
94
+ return
95
+ end
96
+
88
97
  resolver = find_resolver_for(connection)
89
98
  resolver << connection
90
99
  return if resolver.empty?
data/lib/httpx/request.rb CHANGED
@@ -35,14 +35,22 @@ module HTTPX
35
35
 
36
36
  attr_reader :verb, :uri, :headers, :body, :state, :options, :response
37
37
 
38
+ # Exception raised during enumerable body writes
39
+ attr_reader :drain_error
40
+
38
41
  def_delegator :@body, :empty?
39
42
 
40
43
  def_delegator :@body, :chunk!
41
44
 
42
45
  def initialize(verb, uri, options = {})
43
46
  @verb = verb.to_s.downcase.to_sym
44
- @uri = Utils.uri(uri)
45
47
  @options = Options.new(options)
48
+ @uri = Utils.uri(uri)
49
+ if @uri.relative?
50
+ raise(Error, "invalid URI: #{@uri}") unless @options.origin
51
+
52
+ @uri = @options.origin.merge(@uri)
53
+ end
46
54
 
47
55
  raise(Error, "unknown method: #{verb}") unless METHODS.include?(@verb)
48
56
 
@@ -54,6 +62,14 @@ module HTTPX
54
62
  @state = :idle
55
63
  end
56
64
 
65
+ def trailers?
66
+ defined?(@trailers)
67
+ end
68
+
69
+ def trailers
70
+ @trailers ||= @options.headers_class.new
71
+ end
72
+
57
73
  def interests
58
74
  return :r if @state == :done || @state == :expect
59
75
 
@@ -124,6 +140,9 @@ module HTTPX
124
140
  chunk.dup
125
141
  rescue StopIteration
126
142
  nil
143
+ rescue StandardError => e
144
+ @drain_error = e
145
+ nil
127
146
  end
128
147
 
129
148
  # :nocov:
@@ -200,7 +219,9 @@ module HTTPX
200
219
  end
201
220
 
202
221
  def unbounded_body?
203
- chunked? || @body.bytesize == Float::INFINITY
222
+ return @unbounded_body if defined?(@unbounded_body)
223
+
224
+ @unbounded_body = (chunked? || @body.bytesize == Float::INFINITY)
204
225
  end
205
226
 
206
227
  def chunked?
@@ -253,6 +274,8 @@ module HTTPX
253
274
  nextstate = :expect
254
275
  end
255
276
  end
277
+ when :trailers
278
+ return unless @state == :body
256
279
  when :done
257
280
  return if @state == :expect
258
281
  end
@@ -101,7 +101,7 @@ module HTTPX
101
101
  transition(:open)
102
102
  end
103
103
 
104
- !@write_buffer.empty? || @queries.empty? ? :w : :r
104
+ calculate_interests
105
105
  end
106
106
 
107
107
  def <<(connection)
@@ -127,10 +127,14 @@ module HTTPX
127
127
 
128
128
  private
129
129
 
130
+ def calculate_interests
131
+ !@write_buffer.empty? || @queries.empty? ? :w : :r
132
+ end
133
+
130
134
  def consume
131
- dread
135
+ dread if calculate_interests == :r
132
136
  do_retry
133
- dwrite
137
+ dwrite if calculate_interests == :w
134
138
  end
135
139
 
136
140
  def do_retry
@@ -27,8 +27,7 @@ module HTTPX
27
27
  @version = version
28
28
  @status = Integer(status)
29
29
  @headers = @options.headers_class.new(headers)
30
- @body = @options.response_body_class.new(self, threshold_size: @options.body_threshold_size,
31
- window_size: @options.window_size)
30
+ @body = @options.response_body_class.new(self, @options)
32
31
  end
33
32
 
34
33
  def merge_headers(h)
@@ -83,17 +82,22 @@ module HTTPX
83
82
  end
84
83
 
85
84
  class Body
86
- def initialize(response, threshold_size:, window_size: 1 << 14)
85
+ def initialize(response, options)
87
86
  @response = response
88
87
  @headers = response.headers
89
- @threshold_size = threshold_size
90
- @window_size = window_size
88
+ @options = options
89
+ @threshold_size = options.body_threshold_size
90
+ @window_size = options.window_size
91
91
  @encoding = response.content_type.charset || Encoding::BINARY
92
92
  @length = 0
93
93
  @buffer = nil
94
94
  @state = :idle
95
95
  end
96
96
 
97
+ def closed?
98
+ @state == :closed
99
+ end
100
+
97
101
  def write(chunk)
98
102
  return if @state == :closed
99
103
 
data/lib/httpx/session.rb CHANGED
@@ -77,7 +77,7 @@ module HTTPX
77
77
  end
78
78
 
79
79
  def set_connection_callbacks(connection, connections, options)
80
- connection.on(:misdirected) do |misdirected_request|
80
+ connection.only(:misdirected) do |misdirected_request|
81
81
  other_connection = connection.create_idle(ssl: { alpn_protocols: %w[http/1.1] })
82
82
  other_connection.merge(connection)
83
83
  catch(:coalesced) do
@@ -88,11 +88,11 @@ module HTTPX
88
88
  misdirected_request.transition(:idle)
89
89
  other_connection.send(misdirected_request)
90
90
  end
91
- connection.on(:altsvc) do |alt_origin, origin, alt_params|
91
+ connection.only(:altsvc) do |alt_origin, origin, alt_params|
92
92
  other_connection = build_altsvc_connection(connection, connections, alt_origin, origin, alt_params, options)
93
93
  connections << other_connection if other_connection
94
94
  end
95
- connection.on(:exhausted) do
95
+ connection.only(:exhausted) do
96
96
  other_connection = connection.create_idle
97
97
  other_connection.merge(connection)
98
98
  catch(:coalesced) do
@@ -175,12 +175,18 @@ module HTTPX
175
175
  end
176
176
 
177
177
  def send_requests(*requests, options)
178
- connections = []
179
178
  request_options = @options.merge(options)
180
179
 
180
+ connections = _send_requests(requests, request_options)
181
+ receive_requests(requests, connections, request_options)
182
+ end
183
+
184
+ def _send_requests(requests, options)
185
+ connections = []
186
+
181
187
  requests.each do |request|
182
188
  error = catch(:resolve_error) do
183
- connection = find_connection(request, connections, request_options)
189
+ connection = find_connection(request, connections, options)
184
190
  connection.send(request)
185
191
  end
186
192
  next unless error.is_a?(ResolveError)
@@ -188,13 +194,17 @@ module HTTPX
188
194
  request.emit(:response, ErrorResponse.new(request, error, options))
189
195
  end
190
196
 
197
+ connections
198
+ end
199
+
200
+ def receive_requests(requests, connections, options)
191
201
  responses = []
192
202
 
193
203
  begin
194
204
  # guarantee ordered responses
195
205
  loop do
196
206
  request = requests.first
197
- pool.next_tick until (response = fetch_response(request, connections, request_options))
207
+ pool.next_tick until (response = fetch_response(request, connections, options))
198
208
 
199
209
  responses << response
200
210
  requests.shift
@@ -208,7 +218,7 @@ module HTTPX
208
218
  # opportunity to traverse the requests, hence we're returning only a fraction of the errors
209
219
  # we were supposed to. This effectively fetches the existing responses and return them.
210
220
  while (request = requests.shift)
211
- responses << fetch_response(request, connections, request_options)
221
+ responses << fetch_response(request, connections, options)
212
222
  end
213
223
  break
214
224
  end
@@ -20,7 +20,7 @@ module HTTPX::Transcoder
20
20
  @raw.each do |chunk|
21
21
  yield "#{chunk.bytesize.to_s(16)}#{CRLF}#{chunk}#{CRLF}"
22
22
  end
23
- yield "0#{CRLF}#{CRLF}"
23
+ yield "0#{CRLF}"
24
24
  end
25
25
 
26
26
  def respond_to_missing?(meth, *args)
data/lib/httpx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "0.12.0"
4
+ VERSION = "0.14.1"
5
5
  end
data/sig/callbacks.rbs CHANGED
@@ -6,8 +6,10 @@ module HTTPX
6
6
  module Callbacks
7
7
  def on: (Symbol) { (*untyped) -> void } -> void
8
8
  def once: (Symbol) { (*untyped) -> void } -> void
9
+ def only: (Symbol) { (*untyped) -> void } -> void
9
10
  def emit: (Symbol, *untyped) -> void
10
11
 
12
+ def callbacks_for?: (Symbol) -> bool
11
13
  def callbacks: () -> Hash[Symbol, Array[_Callable]]
12
14
  | (Symbol) -> Array[_Callable]
13
15
  end
data/sig/chainable.rbs CHANGED
@@ -18,7 +18,8 @@ module HTTPX
18
18
  | (:cookies) -> Plugins::sessionCookies
19
19
  | (:expect) -> Session
20
20
  | (:follow_redirects) -> Plugins::sessionFollowRedirects
21
- | (:h2c) -> Plugins::sessionH2C
21
+ | (:upgrade) -> Session
22
+ | (:h2c) -> Session
22
23
  | (:multipart) -> Session
23
24
  | (:persistent) -> Plugins::sessionPersistent
24
25
  | (:proxy) -> Plugins::sessionProxy
@@ -4,6 +4,7 @@ module HTTPX
4
4
  include Loggable
5
5
 
6
6
  attr_reader pending: Array[Request]
7
+ attr_reader requests: Array[Request]
7
8
 
8
9
  @options: Options
9
10
  @max_concurrent_requests: Integer
@@ -51,7 +52,7 @@ module HTTPX
51
52
 
52
53
  def disable_pipelining: () -> void
53
54
 
54
- def set_protocol_headers: (Request) -> void
55
+ def set_protocol_headers: (Request) -> _Each[headers_key, String]
55
56
 
56
57
  def headline_uri: (Request) -> String
57
58
 
@@ -59,6 +60,10 @@ module HTTPX
59
60
 
60
61
  def join_headers: (Request request) -> void
61
62
 
63
+ def join_trailers: (Request request) -> void
64
+
65
+ def join_headers2: (_Each[headers_key, String] headers) -> void
66
+
62
67
  def join_body: (Request request) -> void
63
68
 
64
69
  def capitalized: (String field) -> String
@@ -43,7 +43,7 @@ module HTTPX
43
43
 
44
44
  def headline_uri: (Request) -> String
45
45
 
46
- def set_protocol_headers: (Request) -> void
46
+ def set_protocol_headers: (Request) -> _Each[headers_key, String]
47
47
 
48
48
  def handle: (Request request, HTTP2Next::Stream stream) -> void
49
49
 
@@ -53,13 +53,17 @@ module HTTPX
53
53
 
54
54
  def join_headers: (HTTP2Next::Stream stream, Request request) -> void
55
55
 
56
+ def join_trailers: (HTTP2Next::Stream stream, Request request) -> void
57
+
56
58
  def join_body: (HTTP2Next::Stream stream, Request request) -> void
57
59
 
58
60
  def on_stream_headers: (HTTP2Next::Stream stream, Request request, Array[[String, String]] headers) -> void
59
61
 
62
+ def on_stream_trailers: (HTTP2Next::Stream stream, Request request, Array[[String, String]] headers) -> void
63
+
60
64
  def on_stream_data: (HTTP2Next::Stream stream, Request request, string data) -> void
61
65
 
62
- def on_stream_close: (HTTP2Next::Stream stream, Request request, Symbol? error) -> void
66
+ def on_stream_close: (HTTP2Next::Stream stream, Request request, (Symbol | StandardError)? error) -> void
63
67
 
64
68
  def on_frame: (string bytes) -> void
65
69