httpx 0.13.2 → 0.14.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 (53) 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 +2 -2
  4. data/doc/release_notes/0_13_1.md +1 -1
  5. data/doc/release_notes/0_14_0.md +79 -0
  6. data/lib/httpx.rb +1 -2
  7. data/lib/httpx/callbacks.rb +12 -3
  8. data/lib/httpx/connection.rb +12 -9
  9. data/lib/httpx/connection/http1.rb +26 -11
  10. data/lib/httpx/connection/http2.rb +52 -8
  11. data/lib/httpx/headers.rb +1 -1
  12. data/lib/httpx/io/tcp.rb +1 -1
  13. data/lib/httpx/options.rb +91 -56
  14. data/lib/httpx/plugins/aws_sdk_authentication.rb +5 -2
  15. data/lib/httpx/plugins/aws_sigv4.rb +4 -4
  16. data/lib/httpx/plugins/basic_authentication.rb +8 -3
  17. data/lib/httpx/plugins/compression.rb +8 -8
  18. data/lib/httpx/plugins/compression/brotli.rb +4 -3
  19. data/lib/httpx/plugins/compression/deflate.rb +4 -3
  20. data/lib/httpx/plugins/compression/gzip.rb +2 -1
  21. data/lib/httpx/plugins/cookies.rb +3 -7
  22. data/lib/httpx/plugins/digest_authentication.rb +4 -4
  23. data/lib/httpx/plugins/expect.rb +6 -6
  24. data/lib/httpx/plugins/follow_redirects.rb +3 -3
  25. data/lib/httpx/plugins/grpc.rb +247 -0
  26. data/lib/httpx/plugins/grpc/call.rb +62 -0
  27. data/lib/httpx/plugins/grpc/message.rb +85 -0
  28. data/lib/httpx/plugins/multipart/part.rb +1 -1
  29. data/lib/httpx/plugins/proxy.rb +3 -7
  30. data/lib/httpx/plugins/proxy/ssh.rb +3 -3
  31. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  32. data/lib/httpx/plugins/retries.rb +13 -14
  33. data/lib/httpx/plugins/stream.rb +96 -74
  34. data/lib/httpx/plugins/upgrade.rb +4 -4
  35. data/lib/httpx/request.rb +25 -2
  36. data/lib/httpx/response.rb +4 -0
  37. data/lib/httpx/session.rb +17 -7
  38. data/lib/httpx/transcoder/chunker.rb +1 -1
  39. data/lib/httpx/version.rb +1 -1
  40. data/sig/callbacks.rbs +2 -0
  41. data/sig/connection/http1.rbs +4 -0
  42. data/sig/connection/http2.rbs +5 -1
  43. data/sig/options.rbs +9 -2
  44. data/sig/plugins/aws_sdk_authentication.rbs +2 -0
  45. data/sig/plugins/basic_authentication.rbs +2 -0
  46. data/sig/plugins/compression.rbs +2 -2
  47. data/sig/plugins/stream.rbs +17 -16
  48. data/sig/request.rbs +7 -2
  49. data/sig/response.rbs +1 -0
  50. data/sig/session.rbs +4 -0
  51. metadata +38 -35
  52. data/lib/httpx/timeout.rb +0 -67
  53. data/sig/timeout.rbs +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35006c293d6a61a011056400a7934619b869ad32d9287507df8140bc9afd83fb
4
- data.tar.gz: 62152c1c7cc0f7f330396f8e5967b4430226ba50466d6c9c829a52d4eb4902f8
3
+ metadata.gz: abbeaccc55115244f08e7b39aaad174a34dd2e7aeb10b32cdc0563fbefe1b953
4
+ data.tar.gz: a39fc4e5644b21c1265840011e969c36904756573584cba0f805d568a549ec94
5
5
  SHA512:
6
- metadata.gz: 3aef9a00cb4861ae3c4bcf32af947d5c66b9e5bdf545e2cdae30733454a3c140704e36f138857868b4c564c59bb95e16ee0fc4a33500e4035a4c872f4f78421e
7
- data.tar.gz: a1d55b77708ada85b1ec14575ee441369cbe302d7a13e316d3f91efbf8b64a6b2c8cabaa06329759a07190858f271df88462622a2bcddd598823a7d55de61c28
6
+ metadata.gz: 82fa475dcd9ef05ebe90f0b3c742c17c33185410a70eb5af730fce393681b7f9f346decf78349614f77b76257550c0e3056e61f3b5d6cd5efdba0149eff235f6
7
+ data.tar.gz: 64ceb596415b440c99c9df31569fa0ee2f7c94e51787d5e0d84141f1ade0be92e23e2d7011a361c1366b43c4f159525599367009bd4021c358537f11c4605cac
@@ -26,7 +26,7 @@ From now on, both headers and the responnse payload will also appear, so expecte
26
26
  ## Bugfixes
27
27
 
28
28
  * HTTP/2 and HTTP/1.1 exhausted connections now get properly migrated into a new connection;
29
- * HTTP/2 421 responses will now correctly migrate the connection and pendign requests to HTTP/1.1 (a hanging loop was being caused);
29
+ * HTTP/2 421 responses will now correctly migrate the connection and pending requests to HTTP/1.1 (a hanging loop was being caused);
30
30
  * HTTP/2 connection failed with a GOAWAY settings timeout will now return error responses (instead of hanging indefinitely);
31
31
  * Non-IP proxy name-resolving errors will now move on to the next available proxy in the list (instead of hanging indefinitely);
32
32
  * Non-IP DNS resolve errors for `native` and `https` variants will now return the appropriate error response (instead of hanging indefinitely);
@@ -41,7 +41,7 @@ The `:transport_options` are therefore deprecated, and will be moved in a major
41
41
 
42
42
  ## Improvements
43
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.
44
+ Some internal improvements that allow certain 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
45
 
46
46
  Using exceptionless nonblocking connect calls in the supported rubies.
47
47
 
@@ -55,4 +55,4 @@ When passing open IO objects for origins (the `:io` option), `httpx` was still t
55
55
 
56
56
  Fixed usage of `:io` option when passed an "authority/io" hash.
57
57
 
58
- Fixing some issues around trying to connnect to the next available IPAddress when the previous one was unreachable or ETIMEDOUT.
58
+ Fixing some issues around trying to connnect to the next available IPAddress when the previous one was unreachable or ETIMEDOUT.
@@ -2,4 +2,4 @@
2
2
 
3
3
  ## Bugfixes
4
4
 
5
- Rescue `Errno::EALREADY` on calls to `connect_nonblock(exception: false)` (there are exceptionns after all...).
5
+ Rescue `Errno::EALREADY` on calls to `connect_nonblock(exception: false)` (there are exceptions after all...).
@@ -0,0 +1,79 @@
1
+ # 0.14.0
2
+
3
+ ## Features
4
+
5
+ ### GRPC plugin
6
+
7
+ A new plugin, `:grpc`, is now available. This plugin provides a simple DSL to build GRPC services and performing calls using `httpx` under the hood.
8
+
9
+ Example:
10
+
11
+ ```ruby
12
+ require "httpx"
13
+
14
+ grpc = HTTPX.plugin(:grpc)
15
+ helloworld_stub = grpc.build_stub("localhost:4545")
16
+ helloworld_svc = helloworld_stub.rpc(:SayHello, HelloRequest, HelloReply)
17
+ result = helloworld_svc.say_hello(HelloRequest.new(name: "Jack")) #=> HelloReply: "Hello Jack"
18
+ ```
19
+
20
+ You can read more about the `:grpc` plugin in the [wiki](https://honeyryderchuck.gitlab.io/httpx/wiki/GRPC).
21
+
22
+ ### :origin
23
+
24
+ A new `:origin` option is available. You can use it for setting a base URL for subsequent relative paths on that session:
25
+
26
+ ```ruby
27
+ HTTPX.get("/httpbin/get") #=> HTTPX::Error: invalid URI: /httpbin/get
28
+
29
+ httpbin = HTTPX.with(origin: "https://nghttp2.org")
30
+ httpbin.get("/httpbin/get") #=> #<Response:5420 HTTP/2.0 @status=200 ....
31
+ ```
32
+
33
+ **Note!** The origin is **not** for setting base paths, i.e. if you pass it a relative path, it'll be filtered out in subsequent requests (`HTTPX.with(origin: "https://nghttp2.org/httpbin")` will still use only `"https://nghttp2.org"`).
34
+
35
+ ## Improvements
36
+
37
+ * setting an unexpected option will now raise an `HTTPX::Error` with an helpful message, instead of a confusing `NoMethodError`:
38
+
39
+ ```ruby
40
+ HTTPX.with(foo: "bar")
41
+ # before
42
+ #=> NoMethodError
43
+ # after
44
+ #=> HTTPX::Error: unknown option: foo
45
+
46
+ ```
47
+
48
+ * `HTTPX::Options#def_option` (which can be used for setting custom plugin options) can now be passed a full body string (where the argument is `value`), although it still support the block form. This is the recommended approach, as the block form is based on `define_method`, which would make clients unusable inside ractors.
49
+
50
+ * Added support for `:wait_for_handshake` under the `http2_settings` option (`false` by default). HTTP/2 connections complete the protocol handshake before requests are sent. When this option is `true`, requests get send in the initial payload, before the HTTP/2 connection is fully acknowledged.
51
+
52
+ * 441716a5ac0f7707211ebe0048f568cf0b759c3f: The `:stream` plugin has been improved to start streaming the real response as methods are called (instead of a completely separate synchronous one, which is definitely not good):
53
+
54
+ ```ruby
55
+ session = HTTPX.plugin(:stream)
56
+ response = session.get(build_uri("/stream/3"), stream: true)
57
+
58
+ # before
59
+ response.status # this could block indefinitely, if the request truly streams infinitely.
60
+
61
+ # after
62
+ response.status # sends the request, and starts streaming the response until status is available.
63
+ response.each {|chunk|...} # and now you can start yielding the chunks...
64
+ ```
65
+
66
+
67
+ ## Bugfixes
68
+
69
+ * fixed usage of the `:multipart` if `pathname` isn't loaded.
70
+ * fixed HTTP/2 trailers.
71
+ * fixed connection merges with the same origin, which was causing them to be duplicated and breaking further usage. (#125)
72
+ * fixed repeated session callbacks on a connection, by ensure they're set only once.
73
+ * fixed calculation of `content-length` for streaming or chunked compressed requests.
74
+
75
+
76
+ ## Chore
77
+
78
+ * using ruby base container images in CI instead.
79
+ * using truffleruby official container image.
data/lib/httpx.rb CHANGED
@@ -12,12 +12,11 @@ require "httpx/callbacks"
12
12
  require "httpx/loggable"
13
13
  require "httpx/registry"
14
14
  require "httpx/transcoder"
15
- require "httpx/options"
16
- require "httpx/timeout"
17
15
  require "httpx/pool"
18
16
  require "httpx/headers"
19
17
  require "httpx/request"
20
18
  require "httpx/response"
19
+ require "httpx/options"
21
20
  require "httpx/chainable"
22
21
 
23
22
  # Top-Level Namespace
@@ -6,19 +6,28 @@ module HTTPX
6
6
  callbacks(type) << action
7
7
  end
8
8
 
9
- def once(event, &block)
10
- on(event) do |*args, &callback|
9
+ def once(type, &block)
10
+ on(type) do |*args, &callback|
11
11
  block.call(*args, &callback)
12
12
  :delete
13
13
  end
14
14
  end
15
15
 
16
+ def only(type, &block)
17
+ callbacks(type).clear
18
+ on(type, &block)
19
+ end
20
+
16
21
  def emit(type, *args)
17
- callbacks(type).delete_if { |pr| pr[*args] == :delete }
22
+ callbacks(type).delete_if { |pr| :delete == pr[*args] } # rubocop:disable Style/YodaCondition
18
23
  end
19
24
 
20
25
  protected
21
26
 
27
+ def callbacks_for?(type)
28
+ @callbacks.key?(type) && !@callbacks[type].empty?
29
+ end
30
+
22
31
  def callbacks(type = nil)
23
32
  return @callbacks unless type
24
33
 
@@ -69,7 +69,7 @@ module HTTPX
69
69
  end
70
70
 
71
71
  @inflight = 0
72
- @keep_alive_timeout = options.timeout.keep_alive_timeout
72
+ @keep_alive_timeout = options.timeout[:keep_alive_timeout]
73
73
  @keep_alive_timer = nil
74
74
 
75
75
  self.addresses = options.addresses if options.addresses
@@ -129,7 +129,7 @@ module HTTPX
129
129
  end
130
130
 
131
131
  def merge(connection)
132
- @origins += connection.instance_variable_get(:@origins)
132
+ @origins |= connection.instance_variable_get(:@origins)
133
133
  connection.purge_pending do |req|
134
134
  send(req)
135
135
  end
@@ -238,9 +238,9 @@ module HTTPX
238
238
  def timeout
239
239
  return @timeout if defined?(@timeout)
240
240
 
241
- return @options.timeout.connect_timeout if @state == :idle
241
+ return @options.timeout[:connect_timeout] if @state == :idle
242
242
 
243
- @options.timeout.operation_timeout
243
+ @options.timeout[:operation_timeout]
244
244
  end
245
245
 
246
246
  private
@@ -413,7 +413,7 @@ module HTTPX
413
413
  emit(:exhausted)
414
414
  end
415
415
  parser.on(:origin) do |origin|
416
- @origins << origin
416
+ @origins |= [origin]
417
417
  end
418
418
  parser.on(:close) do |force|
419
419
  transition(:closing)
@@ -443,6 +443,7 @@ module HTTPX
443
443
  emit(:misdirected, request)
444
444
  else
445
445
  response = ErrorResponse.new(request, ex, @options)
446
+ request.response = response
446
447
  request.emit(:response, response)
447
448
  end
448
449
  end
@@ -451,7 +452,7 @@ module HTTPX
451
452
  def transition(nextstate)
452
453
  case nextstate
453
454
  when :idle
454
- @timeout = @current_timeout = @options.timeout.connect_timeout
455
+ @timeout = @current_timeout = @options.timeout[:connect_timeout]
455
456
 
456
457
  when :open
457
458
  return if @state == :closed
@@ -463,7 +464,7 @@ module HTTPX
463
464
 
464
465
  send_pending
465
466
 
466
- @timeout = @current_timeout = @options.timeout.operation_timeout
467
+ @timeout = @current_timeout = @options.timeout[:operation_timeout]
467
468
  emit(:open)
468
469
  when :closing
469
470
  return unless @state == :open
@@ -542,12 +543,14 @@ module HTTPX
542
543
  def handle_error(error)
543
544
  parser.handle_error(error) if @parser && parser.respond_to?(:handle_error)
544
545
  while (request = @pending.shift)
545
- request.emit(:response, ErrorResponse.new(request, error, @options))
546
+ response = ErrorResponse.new(request, error, @options)
547
+ request.response = response
548
+ request.emit(:response, response)
546
549
  end
547
550
  end
548
551
 
549
552
  def total_timeout
550
- total = @options.timeout.total_timeout
553
+ total = @options.timeout[:total_timeout]
551
554
 
552
555
  return unless total
553
556
 
@@ -272,23 +272,18 @@ module HTTPX
272
272
  join_headers(request) if request.state == :headers
273
273
  request.transition(:body)
274
274
  join_body(request) if request.state == :body
275
+ request.transition(:trailers)
276
+ # HTTP/1.1 trailers should only work for chunked encoding
277
+ join_trailers(request) if request.body.chunked? && request.state == :trailers
275
278
  request.transition(:done)
276
279
  end
277
280
  end
278
281
 
279
282
  def join_headers(request)
280
- buffer = +""
281
- buffer << "#{request.verb.to_s.upcase} #{headline_uri(request)} HTTP/#{@version.join(".")}" << CRLF
282
- log(color: :yellow) { "<- HEADLINE: #{buffer.chomp.inspect}" }
283
- @buffer << buffer
284
- buffer.clear
283
+ @buffer << "#{request.verb.to_s.upcase} #{headline_uri(request)} HTTP/#{@version.join(".")}" << CRLF
284
+ log(color: :yellow) { "<- HEADLINE: #{@buffer.to_s.chomp.inspect}" }
285
285
  set_protocol_headers(request)
286
- request.headers.each do |field, value|
287
- buffer << "#{capitalized(field)}: #{value}" << CRLF
288
- log(color: :yellow) { "<- HEADER: #{buffer.chomp}" }
289
- @buffer << buffer
290
- buffer.clear
291
- end
286
+ join_headers2(request.headers)
292
287
  log { "<- " }
293
288
  @buffer << CRLF
294
289
  end
@@ -302,6 +297,26 @@ module HTTPX
302
297
  @buffer << chunk
303
298
  throw(:buffer_full, request) if @buffer.full?
304
299
  end
300
+
301
+ raise request.drain_error if request.drain_error
302
+ end
303
+
304
+ def join_trailers(request)
305
+ return unless request.trailers? && request.callbacks_for?(:trailers)
306
+
307
+ join_headers2(request.trailers)
308
+ log { "<- " }
309
+ @buffer << CRLF
310
+ end
311
+
312
+ def join_headers2(headers)
313
+ buffer = "".b
314
+ headers.each do |field, value|
315
+ buffer << "#{capitalized(field)}: #{value}" << CRLF
316
+ log(color: :yellow) { "<- HEADER: #{buffer.chomp}" }
317
+ @buffer << buffer
318
+ buffer.clear
319
+ end
305
320
  end
306
321
 
307
322
  UPCASED = {
@@ -21,14 +21,16 @@ module HTTPX
21
21
 
22
22
  def initialize(buffer, options)
23
23
  @options = Options.new(options)
24
- @max_concurrent_requests = @options.max_concurrent_requests || MAX_CONCURRENT_REQUESTS
25
- @max_requests = @options.max_requests || 0
24
+ @settings = @options.http2_settings
26
25
  @pending = []
27
26
  @streams = {}
28
27
  @drains = {}
29
28
  @pings = []
30
29
  @buffer = buffer
31
30
  @handshake_completed = false
31
+ @wait_for_handshake = @settings.key?(:wait_for_handshake) ? @settings.delete(:wait_for_handshake) : true
32
+ @max_concurrent_requests = @options.max_concurrent_requests || MAX_CONCURRENT_REQUESTS
33
+ @max_requests = @options.max_requests || 0
32
34
  init_connection
33
35
  end
34
36
 
@@ -36,7 +38,11 @@ module HTTPX
36
38
  # waiting for WINDOW_UPDATE frames
37
39
  return :r if @buffer.full?
38
40
 
39
- return :w if @connection.state == :closed
41
+ if @connection.state == :closed
42
+ return unless @handshake_completed
43
+
44
+ return :w
45
+ end
40
46
 
41
47
  unless (@connection.state == :connected && @handshake_completed)
42
48
  return @buffer.empty? ? :r : :rw
@@ -75,9 +81,13 @@ module HTTPX
75
81
  end
76
82
 
77
83
  def can_buffer_more_requests?
78
- @handshake_completed &&
84
+ if @handshake_completed
79
85
  @streams.size < @max_concurrent_requests &&
80
- @streams.size < @max_requests
86
+ @streams.size < @max_requests
87
+ else
88
+ !@wait_for_handshake &&
89
+ @streams.size < @max_concurrent_requests
90
+ end
81
91
  end
82
92
 
83
93
  def send(request)
@@ -140,12 +150,14 @@ module HTTPX
140
150
  join_headers(stream, request) if request.state == :headers
141
151
  request.transition(:body)
142
152
  join_body(stream, request) if request.state == :body
153
+ request.transition(:trailers)
154
+ join_trailers(stream, request) if request.state == :trailers && !request.body.empty?
143
155
  request.transition(:done)
144
156
  end
145
157
  end
146
158
 
147
159
  def init_connection
148
- @connection = HTTP2Next::Client.new(@options.http2_settings)
160
+ @connection = HTTP2Next::Client.new(@settings)
149
161
  @connection.max_streams = @max_requests if @connection.respond_to?(:max_streams=) && @max_requests.positive?
150
162
  @connection.on(:frame, &method(:on_frame))
151
163
  @connection.on(:frame_sent, &method(:on_frame_sent))
@@ -169,6 +181,7 @@ module HTTPX
169
181
  public :reset
170
182
 
171
183
  def handle_stream(stream, request)
184
+ request.on(:refuse, &method(:on_stream_refuse).curry(3)[stream, request])
172
185
  stream.on(:close, &method(:on_stream_close).curry(3)[stream, request])
173
186
  stream.on(:half_close) do
174
187
  log(level: 2) { "#{stream.id}: waiting for response..." }
@@ -193,6 +206,18 @@ module HTTPX
193
206
  stream.headers(request.headers.each, end_stream: request.empty?)
194
207
  end
195
208
 
209
+ def join_trailers(stream, request)
210
+ unless request.trailers?
211
+ stream.data("", end_stream: true) if request.callbacks_for?(:trailers)
212
+ return
213
+ end
214
+
215
+ log(level: 1, color: :yellow) do
216
+ request.trailers.each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
217
+ end
218
+ stream.headers(request.trailers.each, end_stream: true)
219
+ end
220
+
196
221
  def join_body(stream, request)
197
222
  return if request.empty?
198
223
 
@@ -201,13 +226,15 @@ module HTTPX
201
226
  next_chunk = request.drain_body
202
227
  log(level: 1, color: :green) { "#{stream.id}: -> DATA: #{chunk.bytesize} bytes..." }
203
228
  log(level: 2, color: :green) { "#{stream.id}: -> #{chunk.inspect}" }
204
- stream.data(chunk, end_stream: !next_chunk)
205
- if next_chunk && @buffer.full?
229
+ stream.data(chunk, end_stream: !(next_chunk || request.trailers? || request.callbacks_for?(:trailers)))
230
+ if next_chunk && (@buffer.full? || request.body.unbounded_body?)
206
231
  @drains[request] = next_chunk
207
232
  throw(:buffer_full)
208
233
  end
209
234
  chunk = next_chunk
210
235
  end
236
+
237
+ on_stream_refuse(stream, request, request.drain_error) if request.drain_error
211
238
  end
212
239
 
213
240
  ######
@@ -215,6 +242,11 @@ module HTTPX
215
242
  ######
216
243
 
217
244
  def on_stream_headers(stream, request, h)
245
+ if request.response && request.response.version == "2.0"
246
+ on_stream_trailers(stream, request, h)
247
+ return
248
+ end
249
+
218
250
  log(color: :yellow) do
219
251
  h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{v}" }.join("\n")
220
252
  end
@@ -227,12 +259,24 @@ module HTTPX
227
259
  handle(request, stream) if request.expects?
228
260
  end
229
261
 
262
+ def on_stream_trailers(stream, request, h)
263
+ log(color: :yellow) do
264
+ h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{v}" }.join("\n")
265
+ end
266
+ request.response.merge_headers(h)
267
+ end
268
+
230
269
  def on_stream_data(stream, request, data)
231
270
  log(level: 1, color: :green) { "#{stream.id}: <- DATA: #{data.bytesize} bytes..." }
232
271
  log(level: 2, color: :green) { "#{stream.id}: <- #{data.inspect}" }
233
272
  request.response << data
234
273
  end
235
274
 
275
+ def on_stream_refuse(stream, request, error)
276
+ stream.close
277
+ on_stream_close(stream, request, error)
278
+ end
279
+
236
280
  def on_stream_close(stream, request, error)
237
281
  log(level: 2) { "#{stream.id}: closing stream" }
238
282
  @drains.delete(request)