httpx 1.7.2 → 1.7.6

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/doc/release_notes/1_7_3.md +29 -0
  4. data/doc/release_notes/1_7_4.md +42 -0
  5. data/doc/release_notes/1_7_5.md +10 -0
  6. data/doc/release_notes/1_7_6.md +24 -0
  7. data/lib/httpx/adapters/datadog.rb +37 -64
  8. data/lib/httpx/adapters/webmock.rb +3 -4
  9. data/lib/httpx/altsvc.rb +4 -2
  10. data/lib/httpx/connection/http1.rb +26 -18
  11. data/lib/httpx/connection/http2.rb +53 -33
  12. data/lib/httpx/connection.rb +152 -63
  13. data/lib/httpx/io/ssl.rb +20 -8
  14. data/lib/httpx/io/tcp.rb +18 -12
  15. data/lib/httpx/io/unix.rb +13 -9
  16. data/lib/httpx/options.rb +23 -7
  17. data/lib/httpx/parser/http1.rb +14 -4
  18. data/lib/httpx/plugins/auth/digest.rb +2 -1
  19. data/lib/httpx/plugins/auth.rb +23 -9
  20. data/lib/httpx/plugins/brotli.rb +33 -5
  21. data/lib/httpx/plugins/cookies/cookie.rb +34 -11
  22. data/lib/httpx/plugins/cookies/jar.rb +93 -18
  23. data/lib/httpx/plugins/cookies.rb +7 -3
  24. data/lib/httpx/plugins/expect.rb +33 -3
  25. data/lib/httpx/plugins/fiber_concurrency.rb +2 -4
  26. data/lib/httpx/plugins/follow_redirects.rb +7 -1
  27. data/lib/httpx/plugins/h2c.rb +1 -1
  28. data/lib/httpx/plugins/proxy/http.rb +15 -8
  29. data/lib/httpx/plugins/proxy.rb +10 -2
  30. data/lib/httpx/plugins/rate_limiter.rb +19 -19
  31. data/lib/httpx/plugins/retries.rb +17 -9
  32. data/lib/httpx/plugins/ssrf_filter.rb +1 -0
  33. data/lib/httpx/plugins/stream_bidi.rb +6 -0
  34. data/lib/httpx/plugins/tracing.rb +137 -0
  35. data/lib/httpx/pool.rb +7 -9
  36. data/lib/httpx/request.rb +15 -3
  37. data/lib/httpx/resolver/multi.rb +1 -8
  38. data/lib/httpx/resolver/native.rb +2 -2
  39. data/lib/httpx/resolver/resolver.rb +21 -2
  40. data/lib/httpx/resolver/system.rb +3 -1
  41. data/lib/httpx/response.rb +5 -1
  42. data/lib/httpx/selector.rb +19 -16
  43. data/lib/httpx/session.rb +34 -44
  44. data/lib/httpx/timers.rb +4 -0
  45. data/lib/httpx/version.rb +1 -1
  46. data/sig/altsvc.rbs +2 -0
  47. data/sig/chainable.rbs +2 -1
  48. data/sig/connection/http1.rbs +3 -1
  49. data/sig/connection/http2.rbs +11 -4
  50. data/sig/connection.rbs +16 -2
  51. data/sig/io/ssl.rbs +1 -0
  52. data/sig/io/tcp.rbs +2 -2
  53. data/sig/options.rbs +8 -3
  54. data/sig/parser/http1.rbs +1 -1
  55. data/sig/plugins/auth.rbs +5 -2
  56. data/sig/plugins/brotli.rbs +11 -6
  57. data/sig/plugins/cookies/cookie.rbs +3 -2
  58. data/sig/plugins/cookies/jar.rbs +11 -0
  59. data/sig/plugins/cookies.rbs +2 -0
  60. data/sig/plugins/expect.rbs +21 -2
  61. data/sig/plugins/fiber_concurrency.rbs +2 -2
  62. data/sig/plugins/proxy/socks4.rbs +4 -0
  63. data/sig/plugins/rate_limiter.rbs +2 -2
  64. data/sig/plugins/response_cache.rbs +3 -3
  65. data/sig/plugins/retries.rbs +17 -13
  66. data/sig/plugins/tracing.rbs +41 -0
  67. data/sig/pool.rbs +1 -1
  68. data/sig/request.rbs +4 -0
  69. data/sig/resolver/native.rbs +2 -0
  70. data/sig/resolver/resolver.rbs +4 -2
  71. data/sig/resolver/system.rbs +0 -2
  72. data/sig/response/body.rbs +1 -1
  73. data/sig/selector.rbs +7 -2
  74. data/sig/session.rbs +2 -0
  75. data/sig/timers.rbs +2 -0
  76. data/sig/transcoder/gzip.rbs +1 -1
  77. data/sig/transcoder.rbs +0 -2
  78. metadata +13 -3
@@ -14,6 +14,8 @@ module HTTPX
14
14
  @state = :idle
15
15
  @buffer = "".b
16
16
  @headers = {}
17
+ @content_length = nil
18
+ @_has_trailers = @upgrade = false
17
19
  end
18
20
 
19
21
  def <<(chunk)
@@ -25,7 +27,8 @@ module HTTPX
25
27
  @state = :idle
26
28
  @headers = {}
27
29
  @content_length = nil
28
- @_has_trailers = nil
30
+ @_has_trailers = @upgrade = false
31
+ @buffer = @buffer.to_s
29
32
  @buffer.clear
30
33
  end
31
34
 
@@ -34,7 +37,7 @@ module HTTPX
34
37
  end
35
38
 
36
39
  def upgrade_data
37
- @buffer
40
+ @buffer.to_s
38
41
  end
39
42
 
40
43
  private
@@ -55,6 +58,7 @@ module HTTPX
55
58
  end
56
59
 
57
60
  def parse_headline
61
+ #: @type ivar @buffer: String
58
62
  idx = @buffer.index("\n")
59
63
  return unless idx
60
64
 
@@ -75,6 +79,8 @@ module HTTPX
75
79
  headers = @headers
76
80
  buffer = @buffer
77
81
 
82
+ #: @type var buffer: String
83
+
78
84
  while (idx = buffer.index("\n"))
79
85
  # @type var line: String
80
86
  line = buffer.byteslice(0..idx)
@@ -118,17 +124,20 @@ module HTTPX
118
124
 
119
125
  def parse_data
120
126
  if @buffer.respond_to?(:each)
127
+ # @type ivar @buffer: Transcoder::Chunker::Decoder
121
128
  @buffer.each do |chunk|
122
129
  @observer.on_data(chunk)
123
130
  end
124
131
  elsif @content_length
125
- # @type var data: String
132
+ # @type ivar @buffer: String
126
133
  data = @buffer.byteslice(0, @content_length)
134
+ # @type var data: String
127
135
  @buffer = @buffer.byteslice(@content_length..-1) || "".b
128
136
  @content_length -= data.bytesize
129
137
  @observer.on_data(data)
130
138
  data.clear
131
139
  else
140
+ # @type ivar @buffer: String
132
141
  @observer.on_data(@buffer)
133
142
  @buffer.clear
134
143
  end
@@ -152,7 +161,7 @@ module HTTPX
152
161
  tr_encoding.split(/ *, */).each do |encoding|
153
162
  case encoding
154
163
  when "chunked"
155
- @buffer = Transcoder::Chunker::Decoder.new(@buffer, @_has_trailers)
164
+ @buffer = Transcoder::Chunker::Decoder.new(@buffer.to_s, @_has_trailers)
156
165
  end
157
166
  end
158
167
  end
@@ -165,6 +174,7 @@ module HTTPX
165
174
  if @content_length
166
175
  @content_length <= 0
167
176
  elsif @buffer.respond_to?(:finished?)
177
+ # @type ivar @buffer: Transcoder::Chunker::Decoder
168
178
  @buffer.finished?
169
179
  else
170
180
  false
@@ -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
@@ -44,6 +44,7 @@ module HTTPX
44
44
  super
45
45
 
46
46
  @auth_header_value = nil
47
+ @auth_header_value_mtx = Thread::Mutex.new
47
48
  @skip_auth_header_value = false
48
49
  end
49
50
 
@@ -63,7 +64,9 @@ module HTTPX
63
64
  end
64
65
 
65
66
  def reset_auth_header_value!
66
- @auth_header_value = nil
67
+ @auth_header_value_mtx.synchronize do
68
+ @auth_header_value = nil
69
+ end
67
70
  end
68
71
 
69
72
  private
@@ -71,9 +74,11 @@ module HTTPX
71
74
  def send_request(request, *)
72
75
  return super if @skip_auth_header_value || request.authorized?
73
76
 
74
- @auth_header_value ||= generate_auth_token
77
+ auth_header_value = @auth_header_value_mtx.synchronize do
78
+ @auth_header_value ||= generate_auth_token
79
+ end
75
80
 
76
- request.authorize(@auth_header_value) if @auth_header_value
81
+ request.authorize(auth_header_value) if auth_header_value
77
82
 
78
83
  super
79
84
  end
@@ -92,9 +97,11 @@ module HTTPX
92
97
  end
93
98
 
94
99
  module RequestMethods
100
+ attr_reader :auth_token_value
101
+
95
102
  def initialize(*)
96
103
  super
97
- @auth_token_value = nil
104
+ @auth_token_value = @auth_header_value = nil
98
105
  end
99
106
 
100
107
  def authorized?
@@ -102,19 +109,20 @@ module HTTPX
102
109
  end
103
110
 
104
111
  def unauthorize!
105
- return unless (auth_value = @auth_token_value)
112
+ return unless (auth_value = @auth_header_value)
106
113
 
107
114
  @headers.get("authorization").delete(auth_value)
108
115
 
109
- @auth_token_value = nil
116
+ @auth_token_value = @auth_header_value = nil
110
117
  end
111
118
 
112
119
  def authorize(auth_value)
120
+ @auth_header_value = auth_value
113
121
  if (auth_type = @options.auth_header_type)
114
- auth_value = "#{auth_type} #{auth_value}"
122
+ @auth_header_value = "#{auth_type} #{@auth_header_value}"
115
123
  end
116
124
 
117
- @headers.add("authorization", auth_value)
125
+ @headers.add("authorization", @auth_header_value)
118
126
 
119
127
  @auth_token_value = auth_value
120
128
  end
@@ -138,8 +146,14 @@ module HTTPX
138
146
  return unless auth_error?(response, request.options) ||
139
147
  (@options.generate_auth_value_on_retry && @options.generate_auth_value_on_retry.call(response))
140
148
 
149
+ # regenerate token before retry, but only if it's the first request from batch failing.
150
+ # otherwise, it means that the first request already passed here, so this request should
151
+ # use whatever was generated for it.
152
+ @auth_header_value_mtx.synchronize do
153
+ @auth_header_value = generate_auth_token if request.auth_token_value == @auth_header_value
154
+ end
155
+
141
156
  request.unauthorize!
142
- @auth_header_value = generate_auth_token
143
157
  end
144
158
 
145
159
  def auth_error?(response, options)
@@ -3,11 +3,33 @@
3
3
  module HTTPX
4
4
  module Plugins
5
5
  module Brotli
6
+ class Error < HTTPX::Error; end
7
+
6
8
  class Deflater < Transcoder::Deflater
9
+ def initialize(body)
10
+ @compressor = ::Brotli::Compressor.new
11
+ super
12
+ end
13
+
7
14
  def deflate(chunk)
8
- return unless chunk
15
+ return @compressor.process(chunk) << @compressor.flush if chunk
16
+
17
+ @compressor.finish
18
+ end
19
+ end
20
+
21
+ class Inflater
22
+ def initialize(bytesize)
23
+ @inflater = ::Brotli::Decompressor.new
24
+ @bytesize = bytesize
25
+ end
26
+
27
+ def call(chunk)
28
+ buffer = @inflater.process(chunk)
29
+ @bytesize -= chunk.bytesize
30
+ raise Error, "Unexpected end of compressed stream" if @bytesize <= 0 && !@inflater.finished?
9
31
 
10
- ::Brotli.deflate(chunk)
32
+ buffer
11
33
  end
12
34
  end
13
35
 
@@ -30,19 +52,25 @@ module HTTPX
30
52
  module_function
31
53
 
32
54
  def load_dependencies(*)
55
+ gem "brotli", ">= 0.8.0"
33
56
  require "brotli"
34
57
  end
35
58
 
36
59
  def self.extra_options(options)
37
- options.merge(supported_compression_formats: %w[br] + options.supported_compression_formats)
60
+ supported_compression_formats = (%w[br] + options.supported_compression_formats).freeze
61
+ options.merge(
62
+ supported_compression_formats: supported_compression_formats,
63
+ headers: options.headers_class.new(options.headers.merge("accept-encoding" => supported_compression_formats))
64
+ )
38
65
  end
39
66
 
40
67
  def encode(body)
41
68
  Deflater.new(body)
42
69
  end
43
70
 
44
- def decode(_response, **)
45
- ::Brotli.method(:inflate)
71
+ def decode(response, bytesize: nil)
72
+ bytesize ||= response.headers.key?("content-length") ? response.headers["content-length"].to_i : Float::INFINITY
73
+ Inflater.new(bytesize)
46
74
  end
47
75
  end
48
76
  register_plugin :brotli, Brotli
@@ -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)
@@ -46,6 +70,9 @@ module HTTPX
46
70
  module RequestMethods
47
71
  def initialize(*)
48
72
  super
73
+
74
+ @informational_status = nil
75
+
49
76
  return if @body.empty?
50
77
 
51
78
  threshold = @options.expect_threshold_size
@@ -89,7 +116,7 @@ module HTTPX
89
116
  set_request_timeout(:expect_timeout, request, expect_timeout, :expect, %i[body response]) do
90
117
  # expect timeout expired
91
118
  if request.state == :expect && !request.expects?
92
- Expect.no_expect_store << request.origin
119
+ Expect.no_expect_store.add(request.origin)
93
120
  request.headers.delete("expect")
94
121
  consume
95
122
  end
@@ -108,7 +135,10 @@ module HTTPX
108
135
  request.headers.delete("expect")
109
136
  request.transition(:idle)
110
137
  send_request(request, selector, options)
111
- return
138
+
139
+ # recalling itself, in case an error was triggered by the above, and we can
140
+ # verify retriability again.
141
+ return fetch_response(request, selector, options)
112
142
  end
113
143
 
114
144
  response
@@ -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
 
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- InsecureRedirectError = Class.new(Error)
4
+ class InsecureRedirectError < Error
5
+ end
6
+
5
7
  module Plugins
6
8
  #
7
9
  # This plugin adds support for automatically following redirect (status 30X) responses.
@@ -163,6 +165,10 @@ module HTTPX
163
165
  end
164
166
  else
165
167
  send_request(retry_request, selector, options)
168
+
169
+ # recalling itself, in case an error was triggered by the above, and we can
170
+ # verify retriability again.
171
+ return fetch_response(request, selector, options)
166
172
  end
167
173
  nil
168
174
  end
@@ -81,7 +81,7 @@ module HTTPX
81
81
  @parser.upgrade(request, response)
82
82
  @upgrade_protocol = "h2c"
83
83
 
84
- prev_parser.requests.each do |req|
84
+ prev_parser.pending.each do |req|
85
85
  req.transition(:idle)
86
86
  send(req)
87
87
  end