httpx 0.13.2 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
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)