httpx 1.7.4 → 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.
data/lib/httpx/io/unix.rb CHANGED
@@ -14,16 +14,20 @@ module HTTPX
14
14
  @state = :idle
15
15
  @options = options
16
16
  @fallback_protocol = @options.fallback_protocol
17
- if @options.io
18
- @io = case @options.io
19
- when Hash
20
- @options.io[origin.authority]
21
- else
22
- @options.io
23
- end
24
- raise Error, "Given IO objects do not match the request authority" unless @io
17
+ if (io = @options.io)
18
+ io =
19
+ case io
20
+ when Hash
21
+ io[origin.authority]
22
+ else
23
+ io
24
+ end
25
+ raise Error, "Given IO objects do not match the request authority" unless io
26
+
27
+ # @type var io: UNIXSocket
25
28
 
26
- @path = @io.path
29
+ _, @path = io.addr
30
+ @io = io
27
31
  @keep_open = true
28
32
  @state = :connected
29
33
  elsif path
data/lib/httpx/options.rb CHANGED
@@ -202,16 +202,19 @@ module HTTPX
202
202
 
203
203
  REQUEST_BODY_IVARS = %i[@headers].freeze
204
204
 
205
- def ==(other)
206
- super || options_equals?(other)
207
- end
205
+ # checks whether +other+ matches the same connection-level options
206
+ def connection_options_match?(other, ignore_ivars = nil)
207
+ return true if self == other
208
208
 
209
- # checks whether +other+ is equal by comparing the session options
210
- def options_equals?(other, ignore_ivars = REQUEST_BODY_IVARS)
211
209
  # headers and other request options do not play a role, as they are
212
210
  # relevant only for the request.
213
- ivars = instance_variables - ignore_ivars
214
- other_ivars = other.instance_variables - ignore_ivars
211
+ ivars = instance_variables
212
+ ivars.reject! { |iv| REQUEST_BODY_IVARS.include?(iv) }
213
+ ivars.reject! { |iv| ignore_ivars.include?(iv) } if ignore_ivars
214
+
215
+ other_ivars = other.instance_variables
216
+ other_ivars.reject! { |iv| REQUEST_BODY_IVARS.include?(iv) }
217
+ other_ivars.reject! { |iv| ignore_ivars.include?(iv) } if ignore_ivars
215
218
 
216
219
  return false if ivars.size != other_ivars.size
217
220
 
@@ -222,6 +225,19 @@ module HTTPX
222
225
  end
223
226
  end
224
227
 
228
+ RESOLVER_IVARS = %i[
229
+ @resolver_class @resolver_cache @resolver_options
230
+ @resolver_native_class @resolver_system_class @resolver_https_class
231
+ ].freeze
232
+
233
+ # checks whether +other+ matches the same resolver-level options
234
+ def resolver_options_match?(other)
235
+ self == other ||
236
+ RESOLVER_IVARS.all? do |ivar|
237
+ instance_variable_get(ivar) == other.instance_variable_get(ivar)
238
+ end
239
+ end
240
+
225
241
  # returns a HTTPX::Options instance resulting of the merging of +other+ with self.
226
242
  # it may return self if +other+ is self or equal to self.
227
243
  def merge(other)
@@ -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
@@ -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)
@@ -70,6 +70,9 @@ module HTTPX
70
70
  module RequestMethods
71
71
  def initialize(*)
72
72
  super
73
+
74
+ @informational_status = nil
75
+
73
76
  return if @body.empty?
74
77
 
75
78
  threshold = @options.expect_threshold_size
@@ -148,7 +148,7 @@ module HTTPX
148
148
  log { "failed to get response, #{request.retries} tries to go..." }
149
149
  prepare_to_retry(request, response)
150
150
 
151
- if (retry_after = when_to_retry(request, response, options))
151
+ if (retry_after = when_to_retry(request, response, options)) && retry_after.positive?
152
152
  # apply jitter
153
153
  if (jitter = request.options.retry_jitter)
154
154
  retry_after = jitter.call(retry_after)
@@ -89,7 +89,7 @@ module HTTPX::Plugins
89
89
  on(:headers) do
90
90
  # the usual request init time (when not including the connection handshake)
91
91
  # should be the time the request is buffered the first time.
92
- @init_time ||= ::Time.now
92
+ @init_time ||= ::Time.now.utc
93
93
 
94
94
  tracer.start(self)
95
95
  end
@@ -102,7 +102,7 @@ module HTTPX::Plugins
102
102
  # Example is the :ssrf_filter plugin, which raises an error on
103
103
  # initialize if the host is an IP which matches against the known set.
104
104
  # in such cases, we'll just set here right here.
105
- @init_time ||= ::Time.now
105
+ @init_time ||= ::Time.now.utc
106
106
 
107
107
  super
108
108
  end
@@ -113,7 +113,7 @@ module HTTPX::Plugins
113
113
  def initialize(*)
114
114
  super
115
115
 
116
- @init_time = ::Time.now
116
+ @init_time = ::Time.now.utc
117
117
  end
118
118
 
119
119
  def send(request)
@@ -129,7 +129,7 @@ module HTTPX::Plugins
129
129
 
130
130
  # time of initial request(s) is accounted from the moment
131
131
  # the connection is back to :idle, and ready to connect again.
132
- @init_time = ::Time.now
132
+ @init_time = ::Time.now.utc
133
133
  end
134
134
  end
135
135
  end
data/lib/httpx/pool.rb CHANGED
@@ -122,12 +122,6 @@ module HTTPX
122
122
 
123
123
  @max_connections_cond.signal
124
124
  @origin_conds[connection.origin.to_s].signal
125
-
126
- # Observed situations where a session handling multiple requests in a loop
127
- # across multiple threads checks the same connection in and out, while another
128
- # thread which is waiting on the same connection never gets the chance to pick
129
- # it up, because ruby's thread scheduler never switched on to it in the process.
130
- Thread.pass
131
125
  end
132
126
  end
133
127
 
@@ -153,16 +147,20 @@ module HTTPX
153
147
  resolvers = @resolvers[resolver_type]
154
148
 
155
149
  idx = resolvers.find_index do |res|
156
- res.options == options
150
+ res.options.resolver_options_match?(options)
157
151
  end
158
152
  resolvers.delete_at(idx) if idx
159
153
  end || checkout_new_resolver(resolver_type, options)
160
154
  end
161
155
 
162
156
  def checkin_resolver(resolver)
163
- resolver_class = resolver.class
157
+ if resolver.is_a?(Resolver::Multi)
158
+ resolver_class = resolver.resolvers.first.class
159
+ else
160
+ resolver_class = resolver.class
164
161
 
165
- resolver = resolver.multi
162
+ resolver = resolver.multi
163
+ end
166
164
 
167
165
  # a multi requires all sub-resolvers being closed in order to be
168
166
  # correctly checked back in.
data/lib/httpx/request.rb CHANGED
@@ -42,6 +42,9 @@ module HTTPX
42
42
  # The IP address from the peer server.
43
43
  attr_accessor :peer_address
44
44
 
45
+ # the connection the request is currently being sent to (none if before or after transaction)
46
+ attr_writer :connection
47
+
45
48
  attr_writer :persistent
46
49
 
47
50
  attr_reader :active_timeouts
@@ -91,7 +94,7 @@ module HTTPX
91
94
  raise UnsupportedSchemeError, "#{@uri}: #{@uri.scheme}: unsupported URI scheme" unless ALLOWED_URI_SCHEMES.include?(@uri.scheme)
92
95
 
93
96
  @state = :idle
94
- @response = @drainer = @peer_address = @informational_status = nil
97
+ @connection = @response = @drainer = @peer_address = @informational_status = nil
95
98
  @ping = false
96
99
  @persistent = @options.persistent
97
100
  @active_timeouts = []
@@ -274,8 +277,7 @@ module HTTPX
274
277
  when :idle
275
278
  @body.rewind
276
279
  @ping = false
277
- @response = nil
278
- @drainer = nil
280
+ @response = @drainer = nil
279
281
  @active_timeouts.clear
280
282
  when :headers
281
283
  return unless @state == :idle
@@ -321,6 +323,16 @@ module HTTPX
321
323
  callbacks(event).delete(clb)
322
324
  end
323
325
  end
326
+
327
+ def handle_error(error)
328
+ if (connection = @connection)
329
+ connection.on_error(error, self)
330
+ else
331
+ response = ErrorResponse.new(self, error)
332
+ self.response = response
333
+ emit(:response, response)
334
+ end
335
+ end
324
336
  end
325
337
  end
326
338
 
@@ -66,7 +66,7 @@ module HTTPX
66
66
  end
67
67
 
68
68
  def closed?
69
- @state == :closed
69
+ @state == :idle || @state == :closed
70
70
  end
71
71
 
72
72
  def to_io
@@ -112,6 +112,7 @@ module HTTPX
112
112
  def finish!
113
113
  @finished = true
114
114
  @headers.freeze
115
+ @request.connection = nil
115
116
  end
116
117
 
117
118
  # returns whether the response contains body payload.
@@ -282,6 +283,7 @@ module HTTPX
282
283
  @error = error
283
284
  @options = request.options
284
285
  log_exception(@error)
286
+ finish!
285
287
  end
286
288
 
287
289
  # returns the exception full message.
@@ -299,7 +301,9 @@ module HTTPX
299
301
  true
300
302
  end
301
303
 
302
- def finish!; end
304
+ def finish!
305
+ @request.connection = nil
306
+ end
303
307
 
304
308
  # raises the wrapped exception.
305
309
  def raise_for_status
@@ -29,7 +29,7 @@ module HTTPX
29
29
 
30
30
  def_delegator :@timers, :after
31
31
 
32
- def_delegator :@selectables, :empty?
32
+ def_delegator :@selectables, :each
33
33
 
34
34
  def initialize
35
35
  @timers = Timers.new
@@ -37,8 +37,8 @@ module HTTPX
37
37
  @is_timer_interval = false
38
38
  end
39
39
 
40
- def each(&blk)
41
- @selectables.each(&blk)
40
+ def empty?
41
+ @selectables.empty? && @timers.empty?
42
42
  end
43
43
 
44
44
  def next_tick
@@ -81,7 +81,8 @@ module HTTPX
81
81
 
82
82
  def find_resolver(options)
83
83
  res = @selectables.find do |c|
84
- c.is_a?(Resolver::Resolver) && options == c.options
84
+ c.is_a?(Resolver::Resolver) &&
85
+ options.resolver_options_match?(c.options)
85
86
  end
86
87
 
87
88
  res.multi if res
@@ -148,16 +149,18 @@ module HTTPX
148
149
 
149
150
  next(is_closed) if is_closed
150
151
 
151
- io.log(level: 2) do
152
- "[#{io.state}] registering in selector##{object_id} for select (#{interests})#{" for #{interval} seconds" unless interval.nil?}"
153
- end
152
+ if interests
153
+ io.log(level: 2) do
154
+ "[#{io.state}] registering in selector##{object_id} for select (#{interests})#{" for #{interval} seconds" unless interval.nil?}"
155
+ end
154
156
 
155
- if READABLE.include?(interests)
156
- r = r.nil? ? io : (Array(r) << io)
157
- end
157
+ if READABLE.include?(interests)
158
+ r = r.nil? ? io : (Array(r) << io)
159
+ end
158
160
 
159
- if WRITABLE.include?(interests)
160
- w = w.nil? ? io : (Array(w) << io)
161
+ if WRITABLE.include?(interests)
162
+ w = w.nil? ? io : (Array(w) << io)
163
+ end
161
164
  end
162
165
 
163
166
  is_closed
data/lib/httpx/session.rb CHANGED
@@ -325,53 +325,34 @@ module HTTPX
325
325
 
326
326
  # returns the array of HTTPX::Response objects corresponding to the array of HTTPX::Request +requests+.
327
327
  def receive_requests(requests, selector)
328
- responses = [] # : Array[response]
329
-
330
- # guarantee ordered responses
331
- loop do
332
- request = requests.first
333
-
334
- return responses unless request
335
-
336
- catch(:coalesced) { selector.next_tick } until (response = fetch_response(request, selector, request.options))
337
- request.complete!(response)
338
-
339
- responses << response
340
- requests.shift
328
+ waiting = 0
329
+ responses = requests.map do |request|
330
+ fetch_response(request, selector, request.options).tap do |response|
331
+ waiting += 1 if response.nil?
332
+ end
333
+ end
341
334
 
342
- break if requests.empty?
335
+ until waiting.zero? || selector.empty?
336
+ # loop on selector until at least one response has been received.
337
+ catch(:coalesced) { selector.next_tick }
343
338
 
344
- next unless selector.empty?
339
+ responses.each_with_index do |response, idx|
340
+ next unless response.nil?
345
341
 
346
- # in some cases, the pool of connections might have been drained because there was some
347
- # handshake error, and the error responses have already been emitted, but there was no
348
- # opportunity to traverse the requests, hence we're returning only a fraction of the errors
349
- # we were supposed to. This effectively fetches the existing responses and return them.
350
- exit_from_loop = true
342
+ request = requests[idx]
351
343
 
352
- requests_to_remove = [] # : Array[Request]
344
+ response = fetch_response(request, selector, request.options)
353
345
 
354
- requests.each do |req|
355
- response = fetch_response(req, selector, request.options)
346
+ next unless response
356
347
 
357
- if exit_from_loop && response
358
- req.complete!(response)
359
- responses << response
360
- requests_to_remove << req
361
- else
362
- # fetch_response may resend requests. when that happens, we need to go back to the initial
363
- # loop and process the selector. we still do a pass-through on the remainder of requests, so
364
- # that every request that need to be resent, is resent.
365
- exit_from_loop = false
366
-
367
- raise Error, "something went wrong, responses not found and requests not resent" if selector.empty?
368
- end
348
+ request.complete!(response)
349
+ responses[idx] = response
350
+ waiting -= 1
369
351
  end
352
+ end
370
353
 
371
- break if exit_from_loop
354
+ raise Error, "something went wrong, responses not found and requests not resent" unless waiting.zero?
372
355
 
373
- requests -= requests_to_remove
374
- end
375
356
  responses
376
357
  end
377
358
 
@@ -392,7 +373,11 @@ module HTTPX
392
373
  resolver = find_resolver_for(connection, selector)
393
374
 
394
375
  pin(connection, selector)
395
- early_resolve(resolver, connection) || resolver.lazy_resolve(connection)
376
+ if early_resolve(resolver, connection)
377
+ @pool.checkin_resolver(resolver)
378
+ else
379
+ resolver.lazy_resolve(connection)
380
+ end
396
381
  end
397
382
 
398
383
  def early_resolve(resolver, connection)
data/lib/httpx/timers.rb CHANGED
@@ -6,6 +6,10 @@ module HTTPX
6
6
  @intervals = []
7
7
  end
8
8
 
9
+ def empty?
10
+ @intervals.empty?
11
+ end
12
+
9
13
  def after(interval_in_secs, cb = nil, &blk)
10
14
  callback = cb || blk
11
15
 
data/lib/httpx/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- VERSION = "1.7.4"
4
+ VERSION = "1.7.6"
5
5
  end
data/sig/altsvc.rbs CHANGED
@@ -3,6 +3,8 @@ module HTTPX
3
3
  module ConnectionMixin
4
4
  H2_ALTSVC_SCHEMES: Array[String]
5
5
 
6
+ ALTSVC_IGNORE_IVARS: Array[Symbol]
7
+
6
8
  def send: (Request request) -> void
7
9
 
8
10
  def match?: (http_uri uri, Options options) -> bool
@@ -52,7 +52,7 @@ module HTTPX
52
52
 
53
53
  def on_complete: () -> void
54
54
 
55
- def dispatch: () -> void
55
+ def dispatch: (Request request) -> void
56
56
 
57
57
  def ping: () -> void
58
58
 
data/sig/connection.rbs CHANGED
@@ -50,6 +50,7 @@ module HTTPX
50
50
  @altsvc_connection: instance?
51
51
  @sibling: instance?
52
52
  @main_sibling: bool
53
+ @no_more_requests_counter: Integer
53
54
 
54
55
 
55
56
  def addresses: () -> Array[Resolver::Entry]?
@@ -156,6 +157,12 @@ module HTTPX
156
157
 
157
158
  def build_socket: (?Array[Resolver::Entry]? addrs) -> (TCP | SSL | UNIX)
158
159
 
160
+ def ping: () -> void
161
+
162
+ def pong: () -> void
163
+
164
+ def no_more_requests_loop_check: () -> void
165
+
159
166
  def handle_error: (StandardError error, ?Request? request) -> void
160
167
 
161
168
  def force_purge: () -> void
@@ -172,9 +179,9 @@ module HTTPX
172
179
 
173
180
  def set_request_request_timeout: (Request request) -> void
174
181
 
175
- def write_timeout_callback: (Request request, Numeric write_timeout) -> void
182
+ def write_timeout_callback: (Request request, Numeric timeout) -> void
176
183
 
177
- def read_timeout_callback: (Request request, Numeric read_timeout, ?singleton(RequestTimeoutError) error_type) -> void
184
+ def read_timeout_callback: (Request request, Numeric timeout, ?singleton(RequestTimeoutError) error_type) -> void
178
185
 
179
186
  def set_request_timeout: (Symbol label, Request request, Numeric timeout, Symbol start_event, Symbol | Array[Symbol] finish_events) { () -> void } -> void
180
187
 
data/sig/io/ssl.rbs CHANGED
@@ -8,6 +8,7 @@ module HTTPX
8
8
 
9
9
  @ctx: OpenSSL::SSL::SSLContext
10
10
  @verify_hostname: bool
11
+ @sni_hostname: String
11
12
 
12
13
  attr_writer ssl_session: OpenSSL::SSL::Session?
13
14
 
data/sig/io/tcp.rbs CHANGED
@@ -46,7 +46,7 @@ module HTTPX
46
46
 
47
47
  public
48
48
 
49
- def read: (Integer size, ?(Buffer | String) buffer) -> (0 | nil | untyped)
49
+ def read: (Integer size, Buffer | String buffer) -> (0 | nil | untyped)
50
50
 
51
51
  def write: (Buffer buffer) -> Integer?
52
52
 
@@ -67,6 +67,6 @@ module HTTPX
67
67
 
68
68
  def do_transition: (Symbol nextstate) -> void
69
69
 
70
- def log_transition_state: (Symbol nextstate) -> void
70
+ def log_transition_state: (Symbol nextstate) -> String
71
71
  end
72
72
  end