httpx 1.7.1 → 1.7.3

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/doc/release_notes/1_7_2.md +6 -0
  4. data/doc/release_notes/1_7_3.md +29 -0
  5. data/lib/httpx/adapters/webmock.rb +3 -4
  6. data/lib/httpx/connection/http1.rb +0 -1
  7. data/lib/httpx/connection/http2.rb +41 -30
  8. data/lib/httpx/connection.rb +18 -4
  9. data/lib/httpx/plugins/auth/digest.rb +2 -1
  10. data/lib/httpx/plugins/auth.rb +21 -2
  11. data/lib/httpx/plugins/cookies/cookie.rb +34 -11
  12. data/lib/httpx/plugins/cookies/jar.rb +93 -18
  13. data/lib/httpx/plugins/cookies.rb +7 -3
  14. data/lib/httpx/plugins/expect.rb +26 -2
  15. data/lib/httpx/plugins/fiber_concurrency.rb +2 -4
  16. data/lib/httpx/plugins/follow_redirects.rb +3 -1
  17. data/lib/httpx/plugins/ntlm_auth.rb +1 -1
  18. data/lib/httpx/plugins/rate_limiter.rb +19 -19
  19. data/lib/httpx/plugins/retries.rb +11 -7
  20. data/lib/httpx/plugins/ssrf_filter.rb +1 -0
  21. data/lib/httpx/plugins/stream_bidi.rb +17 -1
  22. data/lib/httpx/request.rb +1 -1
  23. data/lib/httpx/resolver/resolver.rb +5 -0
  24. data/lib/httpx/selector.rb +4 -4
  25. data/lib/httpx/session.rb +6 -5
  26. data/lib/httpx/version.rb +1 -1
  27. data/sig/chainable.rbs +1 -1
  28. data/sig/connection/http2.rbs +8 -4
  29. data/sig/connection.rbs +3 -0
  30. data/sig/plugins/auth.rbs +6 -0
  31. data/sig/plugins/cookies/cookie.rbs +3 -2
  32. data/sig/plugins/cookies/jar.rbs +11 -0
  33. data/sig/plugins/cookies.rbs +2 -0
  34. data/sig/plugins/expect.rbs +17 -2
  35. data/sig/plugins/proxy/socks4.rbs +4 -0
  36. data/sig/plugins/rate_limiter.rbs +2 -2
  37. data/sig/plugins/response_cache.rbs +3 -3
  38. data/sig/plugins/retries.rbs +17 -13
  39. data/sig/plugins/stream_bidi.rbs +3 -0
  40. data/sig/request.rbs +1 -0
  41. data/sig/resolver/native.rbs +2 -0
  42. data/sig/resolver/resolver.rbs +2 -2
  43. data/sig/selector.rbs +4 -0
  44. metadata +7 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9c30e22a2d406a61ef87a58fddd607bb2b1fad7b50d837d8db062d4a538a2d2
4
- data.tar.gz: 9c5e1997b9c03434071c59b2deb2b004f7b1a1077c2ccaed03b9e3a1db7aafe5
3
+ metadata.gz: 54c87d9d8b2be0d12570204fd3c60d37f82127b624dde3a233085d5bcb43778c
4
+ data.tar.gz: 8bfec9fadfe697d083d37a9317e9ec9d6e23016f174bd62cabf24275f18fee79
5
5
  SHA512:
6
- metadata.gz: 87d99c4971b99f086f7811be81a8453e71bd2535120b5e560a2fc837d50e8a040e70ab1b2b191beee4b481fcea5063576d8895d318a0089183c21cb59fdd0f24
7
- data.tar.gz: 85469cf5b5822990367f1e5591798a1512de90a60226853da4b4e6b00dde8816d6218dd2aacc9b5d85f84f6cb1637dd4f7b37836863f44144f05329e4fbdf7f5
6
+ metadata.gz: 2c425e8714c36bdca0ab5d068157caa94ca591829466ccdb888e903487aaab50c9a8756eaa53f1cd692a109160deca10ec9ce4405a7690f38149a9e95ec9fb17
7
+ data.tar.gz: 20c6ae3067f1fe187402c743fb1741a131ba09e5039ed86e5c5d2eb8c94d590c84644ca3e6ab5fd3948b635d328bba4149df3a1d94dde3dfbf257bd694730275
data/README.md CHANGED
@@ -46,7 +46,9 @@ And that's the simplest one there is. But you can also do:
46
46
  HTTPX.post("http://example.com", form: { user: "john", password: "pass" })
47
47
 
48
48
  http = HTTPX.with(headers: { "x-my-name" => "joe" })
49
- http.patch("http://example.com/file", body: File.open("path/to/file")) # request body is streamed
49
+ File.open("path/to/file") do |file|
50
+ http.patch("http://example.com/file", body: file) # request body is streamed
51
+ end
50
52
  ```
51
53
 
52
54
  If you want to do some more things with the response, you can get an `HTTPX::Response`:
@@ -0,0 +1,6 @@
1
+ # 1.7.2
2
+
3
+ ## Bugfixes
4
+
5
+ * `:stream_bidi` plugin: when used with the `:retries` plugin, it will skip calling callbacks referencing state from the connection/stream the request was moved from.
6
+ * `:auth` plugin: fix issue causing tokens to be concatenated on non-auth errors.
@@ -0,0 +1,29 @@
1
+ # 1.7.3
2
+
3
+ ## Improvements
4
+
5
+ ### cookies plugin: Jar as CookieStore
6
+
7
+ While previously an implementation detail, the cookie jar from a `:cookie` plugin-enabled session can now be manipulated by the end user:
8
+
9
+ ```ruby
10
+ cookies_sess = HTTPX.plugin(:cookies)
11
+
12
+ jar = cookies.make_jar
13
+
14
+ sess = cookies_ses.with(cookies: jar)
15
+
16
+ # perform requests using sess, get/set/delete cookies in jar
17
+ ```
18
+
19
+ The jar API now closely follows the [Web Cookie Store API](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore), by providing the same set of functions.
20
+
21
+ Some API backwards compatibility is maintained, however since this was an internal implementation detail, this effort isn't meant to be thorough.
22
+
23
+ ## Bugfixes
24
+
25
+ * `http-2`: clear buffered data chunks when receiving a `GOAWAY` stream frame; without this, the client kept sending the corresponding `DATA` frames, despite the peer server making it known that it wouldn't process it. While this is valid HTTP/2, this could increase the connection window until a point where it'd go over the max frame size. this issue was observed during large file uploads where the first request could fail and make the client renegotiate.
26
+ * `webmock` adapter: fixed response body length accounting which was making `response.body.empty?` return true for responses with payload.
27
+ * `:rate_limiter` plugin relies on an internal refactoring to be able to wait for the time suggested by the peer server instead of the potentially relying on custom user logic via own `:retry_after`.
28
+ * `:fiber_concurrency`: fix wrong names for native/system resolver overrides.
29
+ * connection: fix for race condition when closing the connection, where the state only transitions to `closed` after checking the connection back in to the pool, potentially corrupting it if another session meanwhile has picked it up and manipulated it.
@@ -82,6 +82,7 @@ module WebMock
82
82
 
83
83
  def mock!
84
84
  @mocked = true
85
+ @body.mock!
85
86
  end
86
87
 
87
88
  def mocked?
@@ -90,10 +91,8 @@ module WebMock
90
91
  end
91
92
 
92
93
  module ResponseBodyMethods
93
- def decode_chunk(chunk)
94
- return chunk if @response.mocked?
95
-
96
- super
94
+ def mock!
95
+ @inflaters = nil
97
96
  end
98
97
  end
99
98
 
@@ -280,7 +280,6 @@ module HTTPX
280
280
  end
281
281
 
282
282
  def disable_pipelining
283
- return if @requests.empty?
284
283
  # do not disable pipelining if already set to 1 request at a time
285
284
  return if @max_concurrent_requests == 1
286
285
 
@@ -3,8 +3,6 @@
3
3
  require "securerandom"
4
4
  require "http/2"
5
5
 
6
- HTTP2::Connection.__send__(:public, :send_buffer) if HTTP2::VERSION < "1.1.1"
7
-
8
6
  module HTTPX
9
7
  class Connection::HTTP2
10
8
  include Callbacks
@@ -215,9 +213,7 @@ module HTTPX
215
213
  def handle_stream(stream, request)
216
214
  request.on(:refuse, &method(:on_stream_refuse).curry(3)[stream, request])
217
215
  stream.on(:close, &method(:on_stream_close).curry(3)[stream, request])
218
- stream.on(:half_close) do
219
- log(level: 2) { "#{stream.id}: waiting for response..." }
220
- end
216
+ stream.on(:half_close) { on_stream_half_close(stream, request) }
221
217
  stream.on(:altsvc, &method(:on_altsvc).curry(2)[request.origin])
222
218
  stream.on(:headers, &method(:on_stream_headers).curry(3)[stream, request])
223
219
  stream.on(:data, &method(:on_stream_data).curry(3)[stream, request])
@@ -302,7 +298,7 @@ module HTTPX
302
298
  end
303
299
 
304
300
  log(color: :yellow) do
305
- h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{log_redact_headers(v)}" }.join("\n")
301
+ h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{k == ":status" ? v : log_redact_headers(v)}" }.join("\n")
306
302
  end
307
303
  _, status = h.shift
308
304
  headers = request.options.headers_class.new(h)
@@ -331,6 +327,16 @@ module HTTPX
331
327
  stream.close
332
328
  end
333
329
 
330
+ def on_stream_half_close(stream, _request)
331
+ unless stream.send_buffer.empty?
332
+ stream.send_buffer.clear
333
+ stream.data("", end_stream: true)
334
+ end
335
+
336
+ # TODO: omit log line if response already here
337
+ log(level: 2) { "#{stream.id}: waiting for response..." }
338
+ end
339
+
334
340
  def on_stream_close(stream, request, error)
335
341
  return if error == :stream_closed && !@streams.key?(request)
336
342
 
@@ -404,34 +410,39 @@ module HTTPX
404
410
 
405
411
  def on_frame_sent(frame)
406
412
  log(level: 2) { "#{frame[:stream]}: frame was sent!" }
407
- log(level: 2, color: :blue) do
408
- payload =
409
- case frame[:type]
410
- when :data
411
- frame.merge(payload: frame[:payload].bytesize)
412
- when :headers, :ping
413
- frame.merge(payload: log_redact_headers(frame[:payload]))
414
- else
415
- frame
416
- end
417
- "#{frame[:stream]}: #{payload}"
418
- end
413
+ log(level: 2, color: :blue) { "#{frame[:stream]}: #{frame_with_extra_info(frame)}" }
419
414
  end
420
415
 
421
416
  def on_frame_received(frame)
422
417
  log(level: 2) { "#{frame[:stream]}: frame was received!" }
423
- log(level: 2, color: :magenta) do
424
- payload =
425
- case frame[:type]
426
- when :data
427
- frame.merge(payload: frame[:payload].bytesize)
428
- when :headers, :ping
429
- frame.merge(payload: log_redact_headers(frame[:payload]))
430
- else
431
- frame
432
- end
433
- "#{frame[:stream]}: #{payload}"
434
- end
418
+ log(level: 2, color: :magenta) { "#{frame[:stream]}: #{frame_with_extra_info(frame)}" }
419
+ end
420
+
421
+ def frame_with_extra_info(frame)
422
+ case frame[:type]
423
+ when :data
424
+ frame.merge(payload: frame[:payload].bytesize)
425
+ when :headers, :ping
426
+ frame.merge(payload: log_redact_headers(frame[:payload]))
427
+ when :window_update
428
+ connection_or_stream = if (id = frame[:stream]).zero?
429
+ @connection
430
+ else
431
+ @streams.each_value.find { |s| s.id == id }
432
+ end
433
+ if connection_or_stream
434
+ frame.merge(
435
+ local_window: connection_or_stream.local_window,
436
+ remote_window: connection_or_stream.remote_window,
437
+ buffered_amount: connection_or_stream.buffered_amount,
438
+ stream_state: connection_or_stream.state,
439
+ )
440
+ else
441
+ frame
442
+ end
443
+ else
444
+ frame
445
+ end.merge(connection_state: @connection.state)
435
446
  end
436
447
 
437
448
  def on_altsvc(origin, frame)
@@ -227,6 +227,9 @@ module HTTPX
227
227
  consume
228
228
  end
229
229
  nil
230
+ rescue IOError => e
231
+ @write_buffer.clear
232
+ on_io_error(e)
230
233
  rescue StandardError => e
231
234
  @write_buffer.clear
232
235
  on_error(e)
@@ -375,6 +378,11 @@ module HTTPX
375
378
  current_session.deselect_connection(self, current_selector, @cloned)
376
379
  end
377
380
 
381
+ def on_io_error(e)
382
+ on_error(e)
383
+ force_close(true)
384
+ end
385
+
378
386
  def on_error(error, request = nil)
379
387
  if error.is_a?(OperationTimeoutError)
380
388
 
@@ -493,7 +501,7 @@ module HTTPX
493
501
  # flush as many bytes as the sockets allow.
494
502
  #
495
503
  loop do
496
- # buffer has been drainned, mark and exit the write loop.
504
+ # buffer has been drained, mark and exit the write loop.
497
505
  if @write_buffer.empty?
498
506
  # we only mark as drained on the first loop
499
507
  write_drained = write_drained.nil? && @inflight.positive?
@@ -586,6 +594,8 @@ module HTTPX
586
594
  # back to the pending list before the parser is reset.
587
595
  @inflight -= parser_pending_requests.size
588
596
  @pending.unshift(*parser_pending_requests)
597
+
598
+ parser.pending.clear
589
599
  end
590
600
 
591
601
  def build_parser(protocol = @io.protocol)
@@ -721,8 +731,6 @@ module HTTPX
721
731
 
722
732
  # do not deactivate connection in use
723
733
  return if @inflight.positive? || @parser.waiting_for_ping?
724
-
725
- disconnect
726
734
  when :closing
727
735
  return unless connecting? || @state == :open
728
736
 
@@ -740,8 +748,9 @@ module HTTPX
740
748
  return unless @write_buffer.empty?
741
749
 
742
750
  purge_after_closed
743
- disconnect if @pending.empty?
744
751
 
752
+ # TODO: should this raise an error instead?
753
+ return unless @pending.empty?
745
754
  when :already_open
746
755
  nextstate = :open
747
756
  # the first check for given io readiness must still use a timeout.
@@ -758,6 +767,11 @@ module HTTPX
758
767
  end
759
768
  log(level: 3) { "#{@state} -> #{nextstate}" }
760
769
  @state = nextstate
770
+ # post state change
771
+ case nextstate
772
+ when :closed, :inactive
773
+ disconnect
774
+ end
761
775
  end
762
776
 
763
777
  def close_sibling
@@ -8,7 +8,8 @@ module HTTPX
8
8
  module Plugins
9
9
  module Authentication
10
10
  class Digest
11
- Error = Class.new(Error)
11
+ class Error < Error
12
+ end
12
13
 
13
14
  def initialize(user, password, hashed: false, **)
14
15
  @user = user
@@ -69,7 +69,7 @@ module HTTPX
69
69
  private
70
70
 
71
71
  def send_request(request, *)
72
- return super if @skip_auth_header_value
72
+ return super if @skip_auth_header_value || request.authorized?
73
73
 
74
74
  @auth_header_value ||= generate_auth_token
75
75
 
@@ -92,12 +92,31 @@ module HTTPX
92
92
  end
93
93
 
94
94
  module RequestMethods
95
+ def initialize(*)
96
+ super
97
+ @auth_token_value = nil
98
+ end
99
+
100
+ def authorized?
101
+ !@auth_token_value.nil?
102
+ end
103
+
104
+ def unauthorize!
105
+ return unless (auth_value = @auth_token_value)
106
+
107
+ @headers.get("authorization").delete(auth_value)
108
+
109
+ @auth_token_value = nil
110
+ end
111
+
95
112
  def authorize(auth_value)
96
113
  if (auth_type = @options.auth_header_type)
97
114
  auth_value = "#{auth_type} #{auth_value}"
98
115
  end
99
116
 
100
117
  @headers.add("authorization", auth_value)
118
+
119
+ @auth_token_value = auth_value
101
120
  end
102
121
  end
103
122
 
@@ -119,7 +138,7 @@ module HTTPX
119
138
  return unless auth_error?(response, request.options) ||
120
139
  (@options.generate_auth_value_on_retry && @options.generate_auth_value_on_retry.call(response))
121
140
 
122
- request.headers.get("authorization").pop
141
+ request.unauthorize!
123
142
  @auth_header_value = generate_auth_token
124
143
  end
125
144
 
@@ -14,12 +14,14 @@ module HTTPX
14
14
 
15
15
  attr_reader :domain, :path, :name, :value, :created_at
16
16
 
17
+ # assigns a new +path+ to this cookie.
17
18
  def path=(path)
18
19
  path = String(path)
20
+ @for_domain = false
19
21
  @path = path.start_with?("/") ? path : "/"
20
22
  end
21
23
 
22
- # See #domain.
24
+ # assigns a new +domain+ to this cookie.
23
25
  def domain=(domain)
24
26
  domain = String(domain)
25
27
 
@@ -37,6 +39,13 @@ module HTTPX
37
39
  @domain = @domain_name.hostname
38
40
  end
39
41
 
42
+ # checks whether +other+ is the same cookie, i.e. name, value, domain and path are
43
+ # the same.
44
+ def ==(other)
45
+ @name == other.name && @value == other.value &&
46
+ @path == other.path && @domain == other.domain
47
+ end
48
+
40
49
  # Compares the cookie with another. When there are many cookies with
41
50
  # the same name for a URL, the value of the smallest must be used.
42
51
  def <=>(other)
@@ -47,11 +56,29 @@ module HTTPX
47
56
  (@created_at <=> other.created_at).nonzero? || 0
48
57
  end
49
58
 
59
+ def match?(name_or_options)
60
+ case name_or_options
61
+ when String
62
+ @name == name_or_options
63
+ when Hash, Array
64
+ name_or_options.all? { |k, v| respond_to?(k) && send(k) == v }
65
+ else
66
+ false
67
+ end
68
+ end
69
+
50
70
  class << self
51
71
  def new(cookie, *args)
52
- return cookie if cookie.is_a?(self)
72
+ case cookie
73
+ when self
74
+ cookie
75
+ when Array, Hash
76
+ options = Hash[cookie] #: cookie_attributes
77
+ super(options[:name], options[:value], options)
78
+ else
53
79
 
54
- super
80
+ super
81
+ end
55
82
  end
56
83
 
57
84
  # Tests if +target_path+ is under +base_path+ as described in RFC
@@ -84,16 +111,12 @@ module HTTPX
84
111
  end
85
112
  end
86
113
 
87
- def initialize(arg, *attrs)
114
+ def initialize(arg, value, attrs = nil)
88
115
  @created_at = Time.now
89
116
 
90
- if attrs.empty?
91
- attr_hash = Hash.try_convert(arg)
92
- else
93
- @name = arg
94
- @value, attr_hash = attrs
95
- attr_hash = Hash.try_convert(attr_hash)
96
- end
117
+ @name = arg
118
+ @value = value
119
+ attr_hash = Hash.try_convert(attrs)
97
120
 
98
121
  attr_hash.each do |key, val|
99
122
  key = key.downcase.tr("-", "_").to_sym unless key.is_a?(Symbol)
@@ -4,7 +4,12 @@ module HTTPX
4
4
  module Plugins::Cookies
5
5
  # The Cookie Jar
6
6
  #
7
- # It holds a bunch of cookies.
7
+ # It stores and manages cookies for a session, such as i.e. evicting when expired, access methods, or
8
+ # initialization from parsing `Set-Cookie` HTTP header values.
9
+ #
10
+ # It closely follows the [CookieStore API](https://developer.mozilla.org/en-US/docs/Web/API/CookieStore),
11
+ # by implementing the same methods, with a few specific conveniences for this non-browser manipulation use-case.
12
+ #
8
13
  class Jar
9
14
  using URIExtensions
10
15
 
@@ -12,10 +17,14 @@ module HTTPX
12
17
 
13
18
  def initialize_dup(orig)
14
19
  super
20
+ @mtx = orig.instance_variable_get(:@mtx).dup
15
21
  @cookies = orig.instance_variable_get(:@cookies).dup
16
22
  end
17
23
 
24
+ # initializes the cookie store, either empty, or with whatever is passed as +cookies+, which
25
+ # can be an array of HTTPX::Plugins::Cookies::Cookie objects or hashes-or-tuples of cookie attributes.
18
26
  def initialize(cookies = nil)
27
+ @mtx = Thread::Mutex.new
19
28
  @cookies = []
20
29
 
21
30
  cookies.each do |elem|
@@ -32,48 +41,106 @@ module HTTPX
32
41
  end if cookies
33
42
  end
34
43
 
44
+ # parses the `Set-Cookie` header value as +set_cookie+ and does the corresponding updates.
35
45
  def parse(set_cookie)
36
46
  SetCookieParser.call(set_cookie) do |name, value, attrs|
37
- add(Cookie.new(name, value, attrs))
47
+ set(Cookie.new(name, value, attrs))
48
+ end
49
+ end
50
+
51
+ # returns the first HTTPX::Plugins::Cookie::Cookie instance in the store which matches either the name
52
+ # (when String) or all attributes (when a Hash or array of tuples) passed to +name_or_options+
53
+ def get(name_or_options)
54
+ each.find { |ck| ck.match?(name_or_options) }
55
+ end
56
+
57
+ # returns all HTTPX::Plugins::Cookie::Cookie instances in the store which match either the name
58
+ # (when String) or all attributes (when a Hash or array of tuples) passed to +name_or_options+
59
+ def get_all(name_or_options)
60
+ each.select { |ck| ck.match?(name_or_options) } # rubocop:disable Style/SelectByRegexp
61
+ end
62
+
63
+ # when +name+ is a HTTPX::Plugins::Cookie::Cookie, it stores it internally; when +name+ is a String,
64
+ # it creates a cookie with it and the value-or-attributes passed to +value_or_options+.
65
+
66
+ # optionally, +name+ can also be the attributes hash-or-array as long it contains a <tt>:name</tt> field).
67
+ def set(name, value_or_options = nil)
68
+ cookie = case name
69
+ when Cookie
70
+ raise ArgumentError, "there should not be a second argument" if value_or_options
71
+
72
+ name
73
+ when Array, Hash
74
+ raise ArgumentError, "there should not be a second argument" if value_or_options
75
+
76
+ Cookie.new(name)
77
+ else
78
+ raise ArgumentError, "the second argument is required" unless value_or_options
79
+
80
+ Cookie.new(name, value_or_options)
81
+ end
82
+
83
+ synchronize do
84
+ # If the user agent receives a new cookie with the same cookie-name, domain-value, and path-value
85
+ # as a cookie that it has already stored, the existing cookie is evicted and replaced with the new cookie.
86
+ @cookies.delete_if { |ck| ck.name == cookie.name && ck.domain == cookie.domain && ck.path == cookie.path }
87
+
88
+ @cookies << cookie
38
89
  end
39
90
  end
40
91
 
92
+ # @deprecated
41
93
  def add(cookie, path = nil)
94
+ warn "DEPRECATION WARNING: calling `##{__method__}` is deprecated. Use `#set` instead."
42
95
  c = cookie.dup
43
-
44
96
  c.path = path if path && c.path == "/"
97
+ set(c)
98
+ end
45
99
 
46
- # If the user agent receives a new cookie with the same cookie-name, domain-value, and path-value
47
- # as a cookie that it has already stored, the existing cookie is evicted and replaced with the new cookie.
48
- @cookies.delete_if { |ck| ck.name == c.name && ck.domain == c.domain && ck.path == c.path }
49
-
50
- @cookies << c
100
+ # deletes all cookies in the store which match either the name (when String) or all attributes (when a Hash
101
+ # or array of tuples) passed to +name_or_options+.
102
+ #
103
+ # alternatively, of +name_or_options+ is an instance of HTTPX::Plugins::Cookies::Cookiem, it deletes it from the store.
104
+ def delete(name_or_options)
105
+ synchronize do
106
+ case name_or_options
107
+ when Cookie
108
+ @cookies.delete(name_or_options)
109
+ else
110
+ @cookies.delete_if { |ck| ck.match?(name_or_options) }
111
+ end
112
+ end
51
113
  end
52
114
 
115
+ # returns the list of valid cookies which matdh the domain and path from the URI object passed to +uri+.
53
116
  def [](uri)
54
117
  each(uri).sort
55
118
  end
56
119
 
120
+ # enumerates over all stored cookies. if +uri+ is passed, it'll filter out expired cookies and
121
+ # only yield cookies which match its domain and path.
57
122
  def each(uri = nil, &blk)
58
123
  return enum_for(__method__, uri) unless blk
59
124
 
60
- return @cookies.each(&blk) unless uri
125
+ return synchronize { @cookies.each(&blk) } unless uri
61
126
 
62
127
  now = Time.now
63
128
  tpath = uri.path
64
129
 
65
- @cookies.delete_if do |cookie|
66
- if cookie.expired?(now)
67
- true
68
- else
69
- yield cookie if cookie.valid_for_uri?(uri) && Cookie.path_match?(cookie.path, tpath)
70
- false
130
+ synchronize do
131
+ @cookies.delete_if do |cookie|
132
+ if cookie.expired?(now)
133
+ true
134
+ else
135
+ yield cookie if cookie.valid_for_uri?(uri) && Cookie.path_match?(cookie.path, tpath)
136
+ false
137
+ end
71
138
  end
72
139
  end
73
140
  end
74
141
 
75
142
  def merge(other)
76
- cookies_dup = dup
143
+ jar_dup = dup
77
144
 
78
145
  other.each do |elem|
79
146
  cookie = case elem
@@ -85,10 +152,18 @@ module HTTPX
85
152
  Cookie.new(elem)
86
153
  end
87
154
 
88
- cookies_dup.add(cookie)
155
+ jar_dup.set(cookie)
89
156
  end
90
157
 
91
- cookies_dup
158
+ jar_dup
159
+ end
160
+
161
+ private
162
+
163
+ def synchronize(&block)
164
+ return yield if @mtx.owned?
165
+
166
+ @mtx.synchronize(&block)
92
167
  end
93
168
  end
94
169
  end
@@ -7,8 +7,6 @@ module HTTPX
7
7
  #
8
8
  # This plugin implements a persistent cookie jar for the duration of a session.
9
9
  #
10
- # It also adds a *#cookies* helper, so that you can pre-fill the cookies of a session.
11
- #
12
10
  # https://gitlab.com/os85/httpx/wikis/Cookies
13
11
  #
14
12
  module Cookies
@@ -46,6 +44,12 @@ module HTTPX
46
44
  request
47
45
  end
48
46
 
47
+ # factory method to return a Jar to the user, which can then manipulate
48
+ # externally to the session.
49
+ def make_jar(*args)
50
+ Jar.new(*args)
51
+ end
52
+
49
53
  private
50
54
 
51
55
  def set_request_callbacks(request)
@@ -96,7 +100,7 @@ module HTTPX
96
100
  cookies.each do |ck|
97
101
  ck.split(/ *; */).each do |cookie|
98
102
  name, value = cookie.split("=", 2)
99
- jar.add(Cookie.new(name, value))
103
+ jar.set(name, value)
100
104
  end
101
105
  end
102
106
  end
@@ -9,10 +9,34 @@ module HTTPX
9
9
  #
10
10
  module Expect
11
11
  EXPECT_TIMEOUT = 2
12
+ NOEXPECT_STORE_MUTEX = Thread::Mutex.new
13
+
14
+ class Store
15
+ def initialize
16
+ @store = []
17
+ @mutex = Thread::Mutex.new
18
+ end
19
+
20
+ def include?(host)
21
+ @mutex.synchronize { @store.include?(host) }
22
+ end
23
+
24
+ def add(host)
25
+ @mutex.synchronize { @store << host }
26
+ end
27
+
28
+ def delete(host)
29
+ @mutex.synchronize { @store.delete(host) }
30
+ end
31
+ end
12
32
 
13
33
  class << self
14
34
  def no_expect_store
15
- @no_expect_store ||= []
35
+ return Ractor.store_if_absent(:httpx_no_expect_store) { Store.new } if Utils.in_ractor?
36
+
37
+ @no_expect_store ||= NOEXPECT_STORE_MUTEX.synchronize do
38
+ @no_expect_store || Store.new
39
+ end
16
40
  end
17
41
 
18
42
  def extra_options(options)
@@ -89,7 +113,7 @@ module HTTPX
89
113
  set_request_timeout(:expect_timeout, request, expect_timeout, :expect, %i[body response]) do
90
114
  # expect timeout expired
91
115
  if request.state == :expect && !request.expects?
92
- Expect.no_expect_store << request.origin
116
+ Expect.no_expect_store.add(request.origin)
93
117
  request.headers.delete("expect")
94
118
  consume
95
119
  end
@@ -160,9 +160,7 @@ module HTTPX
160
160
  end
161
161
  end
162
162
 
163
- module NativeResolverMethods
164
- private
165
-
163
+ module ResolverNativeMethods
166
164
  def calculate_interests
167
165
  return if @queries.empty?
168
166
 
@@ -172,7 +170,7 @@ module HTTPX
172
170
  end
173
171
  end
174
172
 
175
- module SystemResolverMethods
173
+ module ResolverSystemMethods
176
174
  def interests
177
175
  return unless @queries.any? { |_, conn| conn.current_context? }
178
176