httpx 1.1.5 → 1.2.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -6
  3. data/doc/release_notes/1_1_1.md +2 -2
  4. data/doc/release_notes/1_2_0.md +49 -0
  5. data/doc/release_notes/1_2_1.md +6 -0
  6. data/lib/httpx/adapters/webmock.rb +25 -3
  7. data/lib/httpx/altsvc.rb +57 -2
  8. data/lib/httpx/buffer.rb +8 -0
  9. data/lib/httpx/chainable.rb +48 -29
  10. data/lib/httpx/connection/http1.rb +27 -22
  11. data/lib/httpx/connection/http2.rb +7 -3
  12. data/lib/httpx/connection.rb +52 -62
  13. data/lib/httpx/extensions.rb +0 -15
  14. data/lib/httpx/options.rb +85 -28
  15. data/lib/httpx/plugins/aws_sigv4.rb +2 -2
  16. data/lib/httpx/plugins/basic_auth.rb +1 -1
  17. data/lib/httpx/plugins/callbacks.rb +91 -0
  18. data/lib/httpx/plugins/circuit_breaker.rb +2 -0
  19. data/lib/httpx/plugins/cookies.rb +19 -9
  20. data/lib/httpx/plugins/digest_auth.rb +1 -1
  21. data/lib/httpx/plugins/follow_redirects.rb +11 -0
  22. data/lib/httpx/plugins/grpc.rb +2 -2
  23. data/lib/httpx/plugins/h2c.rb +20 -8
  24. data/lib/httpx/plugins/proxy/socks4.rb +2 -2
  25. data/lib/httpx/plugins/proxy/socks5.rb +2 -2
  26. data/lib/httpx/plugins/proxy.rb +16 -34
  27. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  28. data/lib/httpx/plugins/retries.rb +4 -0
  29. data/lib/httpx/plugins/ssrf_filter.rb +142 -0
  30. data/lib/httpx/plugins/stream.rb +1 -1
  31. data/lib/httpx/plugins/upgrade/h2.rb +1 -1
  32. data/lib/httpx/plugins/upgrade.rb +1 -1
  33. data/lib/httpx/plugins/webdav.rb +1 -1
  34. data/lib/httpx/pool.rb +32 -28
  35. data/lib/httpx/request/body.rb +3 -3
  36. data/lib/httpx/request.rb +3 -5
  37. data/lib/httpx/resolver/https.rb +10 -4
  38. data/lib/httpx/resolver/native.rb +1 -0
  39. data/lib/httpx/resolver/resolver.rb +17 -6
  40. data/lib/httpx/response/body.rb +3 -0
  41. data/lib/httpx/response.rb +3 -2
  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/pool.rbs +1 -3
  58. data/sig/resolver/resolver.rbs +3 -1
  59. data/sig/session.rbs +4 -4
  60. metadata +14 -6
@@ -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,29 @@ 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
+
249
232
  transition(:closed)
250
- emit(:close)
251
233
  end
252
234
 
253
235
  def send(request)
254
236
  if @parser && !@write_buffer.full?
255
- request.headers["alt-used"] = @origin.authority if match_altsvcs?(request.uri)
256
-
257
237
  if @response_received_at && @keep_alive_timeout &&
258
238
  Utils.elapsed_time(@response_received_at) > @keep_alive_timeout
259
239
  # when pushing a request into an existing connection, we have to check whether there
@@ -319,10 +299,6 @@ module HTTPX
319
299
  transition(:open)
320
300
  end
321
301
 
322
- def exhausted?
323
- @parser && parser.exhausted?
324
- end
325
-
326
302
  def consume
327
303
  return unless @io
328
304
 
@@ -497,34 +473,25 @@ module HTTPX
497
473
  request.emit(:promise, parser, stream)
498
474
  end
499
475
  parser.on(:exhausted) do
476
+ @pending.concat(parser.pending)
500
477
  emit(:exhausted)
501
478
  end
502
479
  parser.on(:origin) do |origin|
503
480
  @origins |= [origin]
504
481
  end
505
482
  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
483
+ if force
484
+ reset
485
+ emit(:terminate)
512
486
  end
513
487
  end
514
488
  parser.on(:close_handshake) do
515
489
  consume
516
490
  end
517
491
  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
492
+ @pending.concat(parser.pending) unless parser.empty?
493
+ reset
494
+ idling unless @pending.empty?
528
495
  end
529
496
  parser.on(:current_timeout) do
530
497
  @current_timeout = @timeout = parser.timeout
@@ -593,13 +560,23 @@ module HTTPX
593
560
  when :inactive
594
561
  return unless @state == :open
595
562
  when :closing
596
- return unless @state == :open
597
-
563
+ return unless @state == :idle || @state == :open
564
+
565
+ unless @write_buffer.empty?
566
+ # preset state before handshake, as error callbacks
567
+ # may take it back here.
568
+ @state = nextstate
569
+ # handshakes, try sending
570
+ consume
571
+ @write_buffer.clear
572
+ return
573
+ end
598
574
  when :closed
599
575
  return unless @state == :closing
600
576
  return unless @write_buffer.empty?
601
577
 
602
578
  purge_after_closed
579
+ emit(:close) if @pending.empty?
603
580
  when :already_open
604
581
  nextstate = :open
605
582
  # the first check for given io readiness must still use a timeout.
@@ -621,6 +598,19 @@ module HTTPX
621
598
  @timeout = nil
622
599
  end
623
600
 
601
+ def initialize_type(uri, options)
602
+ options.transport || begin
603
+ case uri.scheme
604
+ when "http"
605
+ "tcp"
606
+ when "https"
607
+ "ssl"
608
+ else
609
+ raise UnsupportedSchemeError, "#{uri}: #{uri.scheme}: unsupported URI scheme"
610
+ end
611
+ end
612
+ end
613
+
624
614
  def build_socket(addrs = nil)
625
615
  case @type
626
616
  when "tcp"
@@ -715,7 +705,7 @@ module HTTPX
715
705
  interval = @timers.after(timeout, callback)
716
706
 
717
707
  Array(finish_events).each do |event|
718
- # clean up reques timeouts if the connection errors out
708
+ # clean up request timeouts if the connection errors out
719
709
  request.once(event) do
720
710
  if @intervals.include?(interval)
721
711
  interval.delete(callback)
@@ -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,
@@ -97,7 +99,7 @@ module HTTPX
97
99
  # :compress_request_body :: whether to auto-decompress response body (defaults to <tt>true</tt>)
98
100
  # :timeout :: hash of timeout configurations (supports <tt>:connect_timeout</tt>, <tt>:settings_timeout</tt>,
99
101
  # <tt>:operation_timeout</tt>, <tt>:keep_alive_timeout</tt>, <tt>:read_timeout</tt>, <tt>:write_timeout</tt>
100
- # and <tt>:request_timeout</tt>
102
+ # and <tt>:request_timeout</tt>
101
103
  # :headers :: hash of HTTP headers (ex: <tt>{ "x-custom-foo" => "bar" }</tt>)
102
104
  # :window_size :: number of bytes to read from a socket
103
105
  # :buffer_size :: internal read and write buffer size in bytes
@@ -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.
@@ -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