httpx 1.1.4 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/doc/release_notes/1_1_4.md +1 -1
  4. data/doc/release_notes/1_1_5.md +12 -0
  5. data/doc/release_notes/1_2_0.md +49 -0
  6. data/lib/httpx/adapters/webmock.rb +29 -8
  7. data/lib/httpx/altsvc.rb +57 -2
  8. data/lib/httpx/chainable.rb +40 -29
  9. data/lib/httpx/connection/http1.rb +27 -22
  10. data/lib/httpx/connection/http2.rb +7 -3
  11. data/lib/httpx/connection.rb +45 -60
  12. data/lib/httpx/extensions.rb +0 -15
  13. data/lib/httpx/options.rb +84 -27
  14. data/lib/httpx/plugins/aws_sigv4.rb +2 -2
  15. data/lib/httpx/plugins/basic_auth.rb +1 -1
  16. data/lib/httpx/plugins/callbacks.rb +91 -0
  17. data/lib/httpx/plugins/circuit_breaker.rb +2 -0
  18. data/lib/httpx/plugins/cookies.rb +19 -9
  19. data/lib/httpx/plugins/digest_auth.rb +1 -1
  20. data/lib/httpx/plugins/follow_redirects.rb +11 -0
  21. data/lib/httpx/plugins/grpc/call.rb +2 -3
  22. data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -0
  23. data/lib/httpx/plugins/grpc.rb +2 -2
  24. data/lib/httpx/plugins/h2c.rb +20 -8
  25. data/lib/httpx/plugins/proxy/socks4.rb +2 -2
  26. data/lib/httpx/plugins/proxy/socks5.rb +2 -2
  27. data/lib/httpx/plugins/proxy.rb +14 -32
  28. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  29. data/lib/httpx/plugins/retries.rb +4 -0
  30. data/lib/httpx/plugins/ssrf_filter.rb +142 -0
  31. data/lib/httpx/plugins/stream.rb +9 -4
  32. data/lib/httpx/plugins/upgrade/h2.rb +1 -1
  33. data/lib/httpx/plugins/upgrade.rb +1 -1
  34. data/lib/httpx/plugins/webdav.rb +1 -1
  35. data/lib/httpx/pool.rb +32 -28
  36. data/lib/httpx/request/body.rb +3 -3
  37. data/lib/httpx/request.rb +12 -3
  38. data/lib/httpx/resolver/https.rb +3 -2
  39. data/lib/httpx/resolver/native.rb +1 -0
  40. data/lib/httpx/resolver/resolver.rb +17 -6
  41. data/lib/httpx/response.rb +1 -1
  42. data/lib/httpx/session.rb +13 -82
  43. data/lib/httpx/timers.rb +3 -10
  44. data/lib/httpx/transcoder.rb +1 -1
  45. data/lib/httpx/version.rb +1 -1
  46. data/sig/altsvc.rbs +33 -0
  47. data/sig/chainable.rbs +1 -0
  48. data/sig/connection/http1.rbs +2 -1
  49. data/sig/connection.rbs +16 -16
  50. data/sig/options.rbs +10 -2
  51. data/sig/plugins/callbacks.rbs +38 -0
  52. data/sig/plugins/cookies.rbs +2 -0
  53. data/sig/plugins/follow_redirects.rbs +2 -0
  54. data/sig/plugins/proxy/socks4.rbs +2 -1
  55. data/sig/plugins/proxy/socks5.rbs +2 -1
  56. data/sig/plugins/proxy.rbs +11 -1
  57. data/sig/plugins/stream.rbs +24 -22
  58. data/sig/pool.rbs +1 -3
  59. data/sig/resolver/resolver.rbs +3 -1
  60. data/sig/session.rbs +4 -4
  61. metadata +12 -4
@@ -47,11 +47,11 @@ module HTTPX
47
47
 
48
48
  attr_accessor :family
49
49
 
50
- def initialize(type, uri, options)
51
- @type = type
50
+ def initialize(uri, options)
52
51
  @origins = [uri.origin]
53
52
  @origin = Utils.to_uri(uri.origin)
54
53
  @options = Options.new(options)
54
+ @type = initialize_type(uri, @options)
55
55
  @window_size = @options.window_size
56
56
  @read_buffer = Buffer.new(@options.buffer_size)
57
57
  @write_buffer = Buffer.new(@options.buffer_size)
@@ -92,18 +92,14 @@ module HTTPX
92
92
  def match?(uri, options)
93
93
  return false if !used? && (@state == :closing || @state == :closed)
94
94
 
95
- return false if exhausted?
96
-
97
95
  (
98
- (
99
- @origins.include?(uri.origin) &&
100
- # if there is more than one origin to match, it means that this connection
101
- # was the result of coalescing. To prevent blind trust in the case where the
102
- # origin came from an ORIGIN frame, we're going to verify the hostname with the
103
- # SSL certificate
104
- (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host)))
105
- ) && @options == options
106
- ) || (match_altsvcs?(uri) && match_altsvc_options?(uri, options))
96
+ @origins.include?(uri.origin) &&
97
+ # if there is more than one origin to match, it means that this connection
98
+ # was the result of coalescing. To prevent blind trust in the case where the
99
+ # origin came from an ORIGIN frame, we're going to verify the hostname with the
100
+ # SSL certificate
101
+ (@origins.size == 1 || @origin == uri.origin || (@io.is_a?(SSL) && @io.verify_hostname(uri.host)))
102
+ ) && @options == options
107
103
  end
108
104
 
109
105
  def expired?
@@ -115,8 +111,6 @@ module HTTPX
115
111
  def mergeable?(connection)
116
112
  return false if @state == :closing || @state == :closed || !@io
117
113
 
118
- return false if exhausted?
119
-
120
114
  return false unless connection.addresses
121
115
 
122
116
  (
@@ -139,7 +133,7 @@ module HTTPX
139
133
  end
140
134
 
141
135
  def create_idle(options = {})
142
- self.class.new(@type, @origin, @options.merge(options))
136
+ self.class.new(@origin, @options.merge(options))
143
137
  end
144
138
 
145
139
  def merge(connection)
@@ -167,24 +161,6 @@ module HTTPX
167
161
  end
168
162
  end
169
163
 
170
- # checks if this is connection is an alternative service of
171
- # +uri+
172
- def match_altsvcs?(uri)
173
- @origins.any? { |origin| uri.altsvc_match?(origin) } ||
174
- AltSvc.cached_altsvc(@origin).any? do |altsvc|
175
- origin = altsvc["origin"]
176
- origin.altsvc_match?(uri.origin)
177
- end
178
- end
179
-
180
- def match_altsvc_options?(uri, options)
181
- return @options == options unless @options.ssl[:hostname] == uri.host
182
-
183
- dup_options = @options.merge(ssl: { hostname: nil })
184
- dup_options.ssl.delete(:hostname)
185
- dup_options == options
186
- end
187
-
188
164
  def connecting?
189
165
  @state == :idle
190
166
  end
@@ -223,7 +199,6 @@ module HTTPX
223
199
  when :closing
224
200
  consume
225
201
  transition(:closed)
226
- emit(:close)
227
202
  when :open
228
203
  consume
229
204
  end
@@ -236,24 +211,33 @@ module HTTPX
236
211
  @parser.close if @parser
237
212
  end
238
213
 
214
+ def terminate
215
+ @connected_at = nil if @state == :closed
216
+
217
+ close
218
+ end
219
+
239
220
  # bypasses the state machine to force closing of connections still connecting.
240
221
  # **only** used for Happy Eyeballs v2.
241
222
  def force_reset
242
223
  @state = :closing
243
224
  transition(:closed)
244
- emit(:close)
245
225
  end
246
226
 
247
227
  def reset
228
+ return if @state == :closing || @state == :closed
229
+
248
230
  transition(:closing)
231
+ unless @write_buffer.empty?
232
+ # handshakes, try sending
233
+ consume
234
+ @write_buffer.clear
235
+ end
249
236
  transition(:closed)
250
- emit(:close)
251
237
  end
252
238
 
253
239
  def send(request)
254
240
  if @parser && !@write_buffer.full?
255
- request.headers["alt-used"] = @origin.authority if match_altsvcs?(request.uri)
256
-
257
241
  if @response_received_at && @keep_alive_timeout &&
258
242
  Utils.elapsed_time(@response_received_at) > @keep_alive_timeout
259
243
  # when pushing a request into an existing connection, we have to check whether there
@@ -319,10 +303,6 @@ module HTTPX
319
303
  transition(:open)
320
304
  end
321
305
 
322
- def exhausted?
323
- @parser && parser.exhausted?
324
- end
325
-
326
306
  def consume
327
307
  return unless @io
328
308
 
@@ -497,34 +477,25 @@ module HTTPX
497
477
  request.emit(:promise, parser, stream)
498
478
  end
499
479
  parser.on(:exhausted) do
480
+ @pending.concat(parser.pending)
500
481
  emit(:exhausted)
501
482
  end
502
483
  parser.on(:origin) do |origin|
503
484
  @origins |= [origin]
504
485
  end
505
486
  parser.on(:close) do |force|
506
- if @state != :closed
507
- transition(:closing)
508
- if force || @state == :idle
509
- transition(:closed)
510
- emit(:close)
511
- end
487
+ if force
488
+ reset
489
+ emit(:terminate)
512
490
  end
513
491
  end
514
492
  parser.on(:close_handshake) do
515
493
  consume
516
494
  end
517
495
  parser.on(:reset) do
518
- if parser.empty?
519
- reset
520
- else
521
- transition(:closing)
522
- transition(:closed)
523
-
524
- @parser.reset if @parser
525
- transition(:idle)
526
- transition(:open)
527
- end
496
+ @pending.concat(parser.pending) unless parser.empty?
497
+ reset
498
+ idling unless @pending.empty?
528
499
  end
529
500
  parser.on(:current_timeout) do
530
501
  @current_timeout = @timeout = parser.timeout
@@ -593,13 +564,14 @@ module HTTPX
593
564
  when :inactive
594
565
  return unless @state == :open
595
566
  when :closing
596
- return unless @state == :open
567
+ return unless @state == :idle || @state == :open
597
568
 
598
569
  when :closed
599
570
  return unless @state == :closing
600
571
  return unless @write_buffer.empty?
601
572
 
602
573
  purge_after_closed
574
+ emit(:close) if @pending.empty?
603
575
  when :already_open
604
576
  nextstate = :open
605
577
  # the first check for given io readiness must still use a timeout.
@@ -621,6 +593,19 @@ module HTTPX
621
593
  @timeout = nil
622
594
  end
623
595
 
596
+ def initialize_type(uri, options)
597
+ options.transport || begin
598
+ case uri.scheme
599
+ when "http"
600
+ "tcp"
601
+ when "https"
602
+ "ssl"
603
+ else
604
+ raise UnsupportedSchemeError, "#{uri}: #{uri.scheme}: unsupported URI scheme"
605
+ end
606
+ end
607
+ end
608
+
624
609
  def build_socket(addrs = nil)
625
610
  case @type
626
611
  when "tcp"
@@ -54,21 +54,6 @@ module HTTPX
54
54
  def origin
55
55
  "#{scheme}://#{authority}"
56
56
  end unless URI::HTTP.method_defined?(:origin)
57
-
58
- def altsvc_match?(uri)
59
- uri = URI.parse(uri)
60
-
61
- origin == uri.origin || begin
62
- case scheme
63
- when "h2"
64
- (uri.scheme == "https" || uri.scheme == "h2") &&
65
- host == uri.host &&
66
- (port || default_port) == (uri.port || uri.default_port)
67
- else
68
- false
69
- end
70
- end
71
- end
72
57
  end
73
58
  end
74
59
  end
data/lib/httpx/options.rb CHANGED
@@ -11,6 +11,7 @@ module HTTPX
11
11
  MAX_BODY_THRESHOLD_SIZE = (1 << 10) * 112 # 112K
12
12
  KEEP_ALIVE_TIMEOUT = 20
13
13
  SETTINGS_TIMEOUT = 10
14
+ CLOSE_HANDSHAKE_TIMEOUT = 10
14
15
  CONNECT_TIMEOUT = READ_TIMEOUT = WRITE_TIMEOUT = 60
15
16
  REQUEST_TIMEOUT = OPERATION_TIMEOUT = nil
16
17
 
@@ -39,6 +40,7 @@ module HTTPX
39
40
  :timeout => {
40
41
  connect_timeout: CONNECT_TIMEOUT,
41
42
  settings_timeout: SETTINGS_TIMEOUT,
43
+ close_handshake_timeout: CLOSE_HANDSHAKE_TIMEOUT,
42
44
  operation_timeout: OPERATION_TIMEOUT,
43
45
  keep_alive_timeout: KEEP_ALIVE_TIMEOUT,
44
46
  read_timeout: READ_TIMEOUT,
@@ -226,44 +228,69 @@ module HTTPX
226
228
  OUT
227
229
  end
228
230
 
229
- REQUEST_IVARS = %i[@params @form @xml @json @body].freeze
230
- private_constant :REQUEST_IVARS
231
+ REQUEST_BODY_IVARS = %i[@headers @params @form @xml @json @body].freeze
231
232
 
232
233
  def ==(other)
233
- ivars = instance_variables | other.instance_variables
234
+ super || options_equals?(other)
235
+ end
236
+
237
+ def options_equals?(other, ignore_ivars = REQUEST_BODY_IVARS)
238
+ # headers and other request options do not play a role, as they are
239
+ # relevant only for the request.
240
+ ivars = instance_variables - ignore_ivars
241
+ other_ivars = other.instance_variables - ignore_ivars
242
+
243
+ return false if ivars.size != other_ivars.size
244
+
245
+ return false if ivars.sort != other_ivars.sort
246
+
234
247
  ivars.all? do |ivar|
235
- case ivar
236
- when :@headers
237
- # currently, this is used to pick up an available matching connection.
238
- # the headers do not play a role, as they are relevant only for the request.
239
- true
240
- when *REQUEST_IVARS
241
- true
242
- else
243
- instance_variable_get(ivar) == other.instance_variable_get(ivar)
244
- end
248
+ instance_variable_get(ivar) == other.instance_variable_get(ivar)
245
249
  end
246
250
  end
247
251
 
252
+ OTHER_LOOKUP = ->(obj, k, ivar_map) {
253
+ case obj
254
+ when Hash
255
+ obj[ivar_map[k]]
256
+ else
257
+ obj.instance_variable_get(k)
258
+ end
259
+ }
248
260
  def merge(other)
249
- raise ArgumentError, "#{other} is not a valid set of options" unless other.respond_to?(:to_hash)
261
+ ivar_map = nil
262
+ other_ivars = case other
263
+ when Hash
264
+ ivar_map = other.keys.to_h { |k| [:"@#{k}", k] }
265
+ ivar_map.keys
266
+ else
267
+ other.instance_variables
268
+ end
269
+
270
+ return self if other_ivars.empty?
250
271
 
251
- h2 = other.to_hash
252
- return self if h2.empty?
272
+ return self if other_ivars.all? { |ivar| instance_variable_get(ivar) == OTHER_LOOKUP[other, ivar, ivar_map] }
253
273
 
254
- h1 = to_hash
274
+ opts = dup
255
275
 
256
- return self if h1 >= h2
276
+ other_ivars.each do |ivar|
277
+ v = OTHER_LOOKUP[other, ivar, ivar_map]
257
278
 
258
- merged = h1.merge(h2) do |_k, v1, v2|
259
- if v1.respond_to?(:merge) && v2.respond_to?(:merge)
260
- v1.merge(v2)
261
- else
262
- v2
279
+ unless v
280
+ opts.instance_variable_set(ivar, v)
281
+ next
263
282
  end
283
+
284
+ v = opts.__send__(:"option_#{ivar[1..-1]}", v)
285
+
286
+ orig_v = instance_variable_get(ivar)
287
+
288
+ v = orig_v.merge(v) if orig_v.respond_to?(:merge) && v.respond_to?(:merge)
289
+
290
+ opts.instance_variable_set(ivar, v)
264
291
  end
265
292
 
266
- self.class.new(merged)
293
+ opts
267
294
  end
268
295
 
269
296
  def to_hash
@@ -272,10 +299,40 @@ module HTTPX
272
299
  end
273
300
  end
274
301
 
275
- def initialize_dup(other)
276
- instance_variables.each do |ivar|
277
- instance_variable_set(ivar, other.instance_variable_get(ivar).dup)
302
+ def extend_with_plugin_classes(pl)
303
+ if defined?(pl::RequestMethods) || defined?(pl::RequestClassMethods)
304
+ @request_class = @request_class.dup
305
+ @request_class.__send__(:include, pl::RequestMethods) if defined?(pl::RequestMethods)
306
+ @request_class.extend(pl::RequestClassMethods) if defined?(pl::RequestClassMethods)
307
+ end
308
+ if defined?(pl::ResponseMethods) || defined?(pl::ResponseClassMethods)
309
+ @response_class = @response_class.dup
310
+ @response_class.__send__(:include, pl::ResponseMethods) if defined?(pl::ResponseMethods)
311
+ @response_class.extend(pl::ResponseClassMethods) if defined?(pl::ResponseClassMethods)
278
312
  end
313
+ if defined?(pl::HeadersMethods) || defined?(pl::HeadersClassMethods)
314
+ @headers_class = @headers_class.dup
315
+ @headers_class.__send__(:include, pl::HeadersMethods) if defined?(pl::HeadersMethods)
316
+ @headers_class.extend(pl::HeadersClassMethods) if defined?(pl::HeadersClassMethods)
317
+ end
318
+ if defined?(pl::RequestBodyMethods) || defined?(pl::RequestBodyClassMethods)
319
+ @request_body_class = @request_body_class.dup
320
+ @request_body_class.__send__(:include, pl::RequestBodyMethods) if defined?(pl::RequestBodyMethods)
321
+ @request_body_class.extend(pl::RequestBodyClassMethods) if defined?(pl::RequestBodyClassMethods)
322
+ end
323
+ if defined?(pl::ResponseBodyMethods) || defined?(pl::ResponseBodyClassMethods)
324
+ @response_body_class = @response_body_class.dup
325
+ @response_body_class.__send__(:include, pl::ResponseBodyMethods) if defined?(pl::ResponseBodyMethods)
326
+ @response_body_class.extend(pl::ResponseBodyClassMethods) if defined?(pl::ResponseBodyClassMethods)
327
+ end
328
+ if defined?(pl::ConnectionMethods)
329
+ @connection_class = @connection_class.dup
330
+ @connection_class.__send__(:include, pl::ConnectionMethods)
331
+ end
332
+ return unless defined?(pl::OptionsMethods)
333
+
334
+ @options_class = @options_class.dup
335
+ @options_class.__send__(:include, pl::OptionsMethods)
279
336
  end
280
337
 
281
338
  private
@@ -5,7 +5,7 @@ module HTTPX
5
5
  #
6
6
  # This plugin adds AWS Sigv4 authentication.
7
7
  #
8
- # https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
8
+ # https://docs.aws.amazon.com/IAM/latest/UserGuide/signing-elements.html
9
9
  #
10
10
  # https://gitlab.com/os85/httpx/wikis/AWS-SigV4
11
11
  #
@@ -185,7 +185,7 @@ module HTTPX
185
185
  def canonical_query
186
186
  params = query.split("&")
187
187
  # params = params.map { |p| p.match(/=/) ? p : p + '=' }
188
- # From: https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
188
+ # From: https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html#create-canonical-request
189
189
  # Sort the parameter names by character code point in ascending order.
190
190
  # Parameters with duplicate names should be sorted by value.
191
191
  #
@@ -3,7 +3,7 @@
3
3
  module HTTPX
4
4
  module Plugins
5
5
  #
6
- # This plugin adds helper methods to implement HTTP Basic Auth (https://tools.ietf.org/html/rfc7617)
6
+ # This plugin adds helper methods to implement HTTP Basic Auth (https://datatracker.ietf.org/doc/html/rfc7617)
7
7
  #
8
8
  # https://gitlab.com/os85/httpx/wikis/Auth#basic-auth
9
9
  #
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Plugins
5
+ #
6
+ # This plugin adds suppoort for callbacks around the request/response lifecycle.
7
+ #
8
+ # https://gitlab.com/os85/httpx/-/wikis/Events
9
+ #
10
+ module Callbacks
11
+ # connection closed user-space errors happen after errors can be surfaced to requests,
12
+ # so they need to pierce through the scheduler, which is only possible by simulating an
13
+ # interrupt.
14
+ class CallbackError < Exception; end # rubocop:disable Lint/InheritException
15
+
16
+ module InstanceMethods
17
+ include HTTPX::Callbacks
18
+
19
+ %i[
20
+ connection_opened connection_closed
21
+ request_error
22
+ request_started request_body_chunk request_completed
23
+ response_started response_body_chunk response_completed
24
+ ].each do |meth|
25
+ class_eval(<<-MOD, __FILE__, __LINE__ + 1)
26
+ def on_#{meth}(&blk) # def on_connection_opened(&blk)
27
+ on(:#{meth}, &blk) # on(:connection_opened, &blk)
28
+ end # end
29
+ MOD
30
+ end
31
+
32
+ private
33
+
34
+ def init_connection(uri, options)
35
+ connection = super
36
+ connection.on(:open) do
37
+ emit_or_callback_error(:connection_opened, connection.origin, connection.io.socket)
38
+ end
39
+ connection.on(:close) do
40
+ emit_or_callback_error(:connection_closed, connection.origin) if connection.used?
41
+ end
42
+
43
+ connection
44
+ end
45
+
46
+ def set_request_callbacks(request)
47
+ super
48
+
49
+ request.on(:headers) do
50
+ emit_or_callback_error(:request_started, request)
51
+ end
52
+ request.on(:body_chunk) do |chunk|
53
+ emit_or_callback_error(:request_body_chunk, request, chunk)
54
+ end
55
+ request.on(:done) do
56
+ emit_or_callback_error(:request_completed, request)
57
+ end
58
+
59
+ request.on(:response_started) do |res|
60
+ if res.is_a?(Response)
61
+ emit_or_callback_error(:response_started, request, res)
62
+ res.on(:chunk_received) do |chunk|
63
+ emit_or_callback_error(:response_body_chunk, request, res, chunk)
64
+ end
65
+ else
66
+ emit_or_callback_error(:request_error, request, res.error)
67
+ end
68
+ end
69
+ request.on(:response) do |res|
70
+ emit_or_callback_error(:response_completed, request, res)
71
+ end
72
+ end
73
+
74
+ def emit_or_callback_error(*args)
75
+ emit(*args)
76
+ rescue StandardError => e
77
+ ex = CallbackError.new(e.message)
78
+ ex.set_backtrace(e.backtrace)
79
+ raise ex
80
+ end
81
+
82
+ def receive_requests(*)
83
+ super
84
+ rescue CallbackError => e
85
+ raise e.cause
86
+ end
87
+ end
88
+ end
89
+ register_plugin :callbacks, Callbacks
90
+ end
91
+ end
@@ -25,6 +25,8 @@ module HTTPX
25
25
  end
26
26
 
27
27
  module InstanceMethods
28
+ include HTTPX::Callbacks
29
+
28
30
  def initialize(*)
29
31
  super
30
32
  @circuit_store = CircuitStore.new(@options)
@@ -71,22 +71,32 @@ module HTTPX
71
71
  end
72
72
 
73
73
  module OptionsMethods
74
- def do_initialize(*)
75
- super
74
+ def option_headers(*)
75
+ value = super
76
+
77
+ merge_cookie_in_jar(value.delete("cookie"), @cookies) if defined?(@cookies) && value.key?("cookie")
76
78
 
77
- return unless @headers.key?("cookie")
79
+ value
80
+ end
78
81
 
79
- @headers.delete("cookie").each do |ck|
82
+ def option_cookies(value)
83
+ jar = value.is_a?(Jar) ? value : Jar.new(value)
84
+
85
+ merge_cookie_in_jar(@headers.delete("cookie"), jar) if defined?(@headers) && @headers.key?("cookie")
86
+
87
+ jar
88
+ end
89
+
90
+ private
91
+
92
+ def merge_cookie_in_jar(cookies, jar)
93
+ cookies.each do |ck|
80
94
  ck.split(/ *; */).each do |cookie|
81
95
  name, value = cookie.split("=", 2)
82
- @cookies.add(Cookie.new(name, value))
96
+ jar.add(Cookie.new(name, value))
83
97
  end
84
98
  end
85
99
  end
86
-
87
- def option_cookies(value)
88
- value.is_a?(Jar) ? value : Jar.new(value)
89
- end
90
100
  end
91
101
  end
92
102
  register_plugin :cookies, Cookies
@@ -3,7 +3,7 @@
3
3
  module HTTPX
4
4
  module Plugins
5
5
  #
6
- # This plugin adds helper methods to implement HTTP Digest Auth (https://tools.ietf.org/html/rfc7616)
6
+ # This plugin adds helper methods to implement HTTP Digest Auth (https://datatracker.ietf.org/doc/html/rfc7616)
7
7
  #
8
8
  # https://gitlab.com/os85/httpx/wikis/Auth#digest-auth
9
9
  #
@@ -34,6 +34,12 @@ module HTTPX
34
34
  def option_allow_auth_to_other_origins(value)
35
35
  value
36
36
  end
37
+
38
+ def option_redirect_on(value)
39
+ raise TypeError, ":redirect_on must be callable" unless value.respond_to?(:call)
40
+
41
+ value
42
+ end
37
43
  end
38
44
 
39
45
  module InstanceMethods
@@ -57,6 +63,11 @@ module HTTPX
57
63
  # build redirect request
58
64
  redirect_uri = __get_location_from_response(response)
59
65
 
66
+ if options.redirect_on
67
+ redirect_allowed = options.redirect_on.call(redirect_uri)
68
+ return response unless redirect_allowed
69
+ end
70
+
60
71
  if response.status == 305 && options.respond_to?(:proxy)
61
72
  # The requested resource MUST be accessed through the proxy given by
62
73
  # the Location field. The Location field gives the URI of the proxy.
@@ -11,6 +11,7 @@ module HTTPX
11
11
  @response = response
12
12
  @decoder = ->(z) { z }
13
13
  @consumed = false
14
+ @grpc_response = nil
14
15
  end
15
16
 
16
17
  def inspect
@@ -34,9 +35,7 @@ module HTTPX
34
35
  private
35
36
 
36
37
  def grpc_response
37
- return @grpc_response if defined?(@grpc_response)
38
-
39
- @grpc_response = if @response.respond_to?(:each)
38
+ @grpc_response ||= if @response.respond_to?(:each)
40
39
  Enumerator.new do |y|
41
40
  Message.stream(@response).each do |message|
42
41
  y << @decoder.call(message)
@@ -37,6 +37,7 @@ module HTTPX
37
37
  class Inflater
38
38
  def initialize(response)
39
39
  @response = response
40
+ @grpc_encodings = nil
40
41
  end
41
42
 
42
43
  def call(message, &blk)
@@ -261,14 +261,14 @@ module HTTPX
261
261
  headers["grpc-timeout"] = "#{deadline}m"
262
262
  end
263
263
 
264
- headers = headers.merge(metadata) if metadata
264
+ headers = headers.merge(metadata.transform_keys(&:to_s)) if metadata
265
265
 
266
266
  # prepare compressor
267
267
  compression = @options.grpc_compression == true ? "gzip" : @options.grpc_compression
268
268
 
269
269
  headers["grpc-encoding"] = compression if compression
270
270
 
271
- headers.merge!(@options.call_credentials.call) if @options.call_credentials
271
+ headers.merge!(@options.call_credentials.call.transform_keys(&:to_s)) if @options.call_credentials
272
272
 
273
273
  build_request("POST", uri, headers: headers, body: input)
274
274
  end