httpx 1.7.2 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c67e4695d8ef368321f14a3c113daad0e49280413e3da42272644ad84fe6f622
4
- data.tar.gz: 62bb1ab9d91ca69c9b1fa561051ffb0ed137afb53e0b40f71cfd477a84a78773
3
+ metadata.gz: 54c87d9d8b2be0d12570204fd3c60d37f82127b624dde3a233085d5bcb43778c
4
+ data.tar.gz: 8bfec9fadfe697d083d37a9317e9ec9d6e23016f174bd62cabf24275f18fee79
5
5
  SHA512:
6
- metadata.gz: 266576dae6b8ed604228b464281239b760df9682d27e9010b18ac7dbab4fb00f23115e5bcae1a0ecee6b191ee03a0eca758c37f8f0baee0e2ad9df5ca0c7eff6
7
- data.tar.gz: e355df3c634811d0d8e08bcb10b661a4f486073e6ca808ca3af3585a4d014cd404c8d7a55e38d0d05419f205d9dc81a54fb5a17eab7e1716c38c160297963e0f
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,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
@@ -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
 
@@ -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.
@@ -16,25 +16,7 @@ module HTTPX
16
16
 
17
17
  class << self
18
18
  def load_dependencies(klass)
19
- klass.plugin(:retries, retry_after: method(:retry_after_rate_limit))
20
- end
21
-
22
- # Servers send the "Retry-After" header field to indicate how long the
23
- # user agent ought to wait before making a follow-up request. When
24
- # sent with a 503 (Service Unavailable) response, Retry-After indicates
25
- # how long the service is expected to be unavailable to the client.
26
- # When sent with any 3xx (Redirection) response, Retry-After indicates
27
- # the minimum time that the user agent is asked to wait before issuing
28
- # the redirected request.
29
- #
30
- def retry_after_rate_limit(_, response)
31
- return unless response.is_a?(Response)
32
-
33
- retry_after = response.headers["retry-after"]
34
-
35
- return unless retry_after
36
-
37
- Utils.parse_retry_after(retry_after)
19
+ klass.plugin(:retries)
38
20
  end
39
21
  end
40
22
 
@@ -52,6 +34,24 @@ module HTTPX
52
34
  def rate_limit_error?(response)
53
35
  response.is_a?(Response) && RATE_LIMIT_CODES.include?(response.status)
54
36
  end
37
+
38
+ # Servers send the "Retry-After" header field to indicate how long the
39
+ # user agent ought to wait before making a follow-up request. When
40
+ # sent with a 503 (Service Unavailable) response, Retry-After indicates
41
+ # how long the service is expected to be unavailable to the client.
42
+ # When sent with any 3xx (Redirection) response, Retry-After indicates
43
+ # the minimum time that the user agent is asked to wait before issuing
44
+ # the redirected request.
45
+ #
46
+ def when_to_retry(_, response, options)
47
+ return super unless response.is_a?(Response)
48
+
49
+ retry_after = response.headers["retry-after"]
50
+
51
+ return super unless retry_after
52
+
53
+ Utils.parse_retry_after(retry_after)
54
+ end
55
55
  end
56
56
  end
57
57
 
@@ -148,10 +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
- retry_after = options.retry_after
152
- retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
153
-
154
- if retry_after
151
+ if (retry_after = when_to_retry(request, response, options))
155
152
  # apply jitter
156
153
  if (jitter = request.options.retry_jitter)
157
154
  retry_after = jitter.call(retry_after)
@@ -201,6 +198,12 @@ module HTTPX
201
198
  request.transition(:idle)
202
199
  end
203
200
 
201
+ def when_to_retry(request, response, options)
202
+ retry_after = options.retry_after
203
+ retry_after = retry_after.call(request, response) if retry_after.respond_to?(:call)
204
+ retry_after
205
+ end
206
+
204
207
  #
205
208
  # Attempt to set the request to perform a partial range request.
206
209
  # This happens if the peer server accepts byte-range requests, and
@@ -237,14 +240,15 @@ module HTTPX
237
240
  def initialize(*args)
238
241
  super
239
242
  @retries = @options.max_retries
243
+ @partial_response = nil
240
244
  end
241
245
 
242
246
  def response=(response)
243
- if @partial_response
247
+ if (partial_response = @partial_response)
244
248
  if response.is_a?(Response) && response.status == 206
245
- response.from_partial_response(@partial_response)
249
+ response.from_partial_response(partial_response)
246
250
  else
247
- @partial_response.close
251
+ partial_response.close
248
252
  end
249
253
  @partial_response = nil
250
254
  end
@@ -106,6 +106,7 @@ module HTTPX
106
106
  error = ServerSideRequestForgeryError.new("#{request.uri} URI scheme not allowed")
107
107
  error.set_backtrace(caller)
108
108
  response = ErrorResponse.new(request, error)
109
+ request.response = response
109
110
  request.emit(:response, response)
110
111
  response
111
112
  end
@@ -189,6 +189,10 @@ module HTTPX
189
189
  !@closed
190
190
  end
191
191
 
192
+ def force_close(*)
193
+ terminate
194
+ end
195
+
192
196
  def terminate
193
197
  return if @closed
194
198
 
@@ -202,6 +206,8 @@ module HTTPX
202
206
  terminate
203
207
  end
204
208
 
209
+ alias_method :on_io_error, :on_error
210
+
205
211
  # noop (the owner connection will take of it)
206
212
  def handle_socket_timeout(interval); end
207
213
  end
data/lib/httpx/request.rb CHANGED
@@ -91,7 +91,7 @@ module HTTPX
91
91
  raise UnsupportedSchemeError, "#{@uri}: #{@uri.scheme}: unsupported URI scheme" unless ALLOWED_URI_SCHEMES.include?(@uri.scheme)
92
92
 
93
93
  @state = :idle
94
- @response = @peer_address = @informational_status = nil
94
+ @response = @drainer = @peer_address = @informational_status = nil
95
95
  @ping = false
96
96
  @persistent = @options.persistent
97
97
  @active_timeouts = []
@@ -119,6 +119,11 @@ module HTTPX
119
119
  end
120
120
  end
121
121
 
122
+ def on_io_error(e)
123
+ on_error(e)
124
+ force_close(true)
125
+ end
126
+
122
127
  def on_error(error)
123
128
  handle_error(error)
124
129
  disconnect
@@ -204,8 +204,7 @@ module HTTPX
204
204
  rescue IOError => e
205
205
  (Array(r) + Array(w)).each do |sel|
206
206
  # TODO: is there a way to cheaply find the IO associated with the error?
207
- sel.on_error(e)
208
- sel.force_close(true)
207
+ sel.on_io_error(e)
209
208
  end
210
209
  rescue StandardError => e
211
210
  (Array(r) + Array(w)).each do |sel|
@@ -249,8 +248,9 @@ module HTTPX
249
248
  when :rw then rw_wait(io, interval)
250
249
  end
251
250
  rescue IOError => e
252
- io.on_error(e)
253
- io.force_close(true)
251
+ io.on_io_error(e)
252
+
253
+ return
254
254
  rescue StandardError => e
255
255
  io.on_error(e)
256
256
 
data/lib/httpx/session.rb CHANGED
@@ -140,9 +140,10 @@ module HTTPX
140
140
  end
141
141
  selector.deregister(connection)
142
142
 
143
+ # do not check-in connections only created for Happy Eyeballs
143
144
  return if cloned
144
145
 
145
- return if @closing && connection.state == :closed
146
+ return if @closing && connection.state == :closed && !connection.used?
146
147
 
147
148
  connection.log(level: 2) { "check-in connection##{connection.object_id}(#{connection.state}) in pool##{@pool.object_id}" }
148
149
  @pool.checkin_connection(connection)
@@ -177,16 +178,15 @@ module HTTPX
177
178
 
178
179
  # returns the HTTPX::Connection through which the +request+ should be sent through.
179
180
  def find_connection(request_uri, selector, options)
180
- log(level: 2) { "finding connection for #{request_uri}..." }
181
181
  if (connection = selector.find_connection(request_uri, options))
182
182
  connection.idling if connection.state == :closed
183
- connection.log(level: 2) { "found connection##{connection.object_id}(#{connection.state}) in selector##{selector.object_id}" }
183
+ log(level: 2) { "found connection##{connection.object_id}(#{connection.state}) in selector##{selector.object_id}" }
184
184
  return connection
185
185
  end
186
186
 
187
187
  connection = @pool.checkout_connection(request_uri, options)
188
188
 
189
- connection.log(level: 2) { "found connection##{connection.object_id}(#{connection.state}) in pool##{@pool.object_id}" }
189
+ log(level: 2) { "found connection##{connection.object_id}(#{connection.state}) in pool##{@pool.object_id}" }
190
190
 
191
191
  case connection.state
192
192
  when :idle
@@ -240,7 +240,7 @@ module HTTPX
240
240
 
241
241
  return unless response && response.finished?
242
242
 
243
- log(level: 2) { "response fetched" }
243
+ log(level: 2) { "response##{response.object_id} fetched" }
244
244
 
245
245
  response
246
246
  end
@@ -249,6 +249,7 @@ module HTTPX
249
249
  def send_request(request, selector, options = request.options)
250
250
  error = begin
251
251
  catch(:resolve_error) do
252
+ log(level: 2) { "finding connection for request##{request.object_id}..." }
252
253
  connection = find_connection(request.uri, selector, options)
253
254
  connection.send(request)
254
255
  end
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.2"
4
+ VERSION = "1.7.3"
5
5
  end
data/sig/chainable.rbs CHANGED
@@ -29,7 +29,7 @@ module HTTPX
29
29
  | (:proxy, ?options) -> (Plugins::sessionProxy & Plugins::httpProxy)
30
30
  | (:push_promise, ?options) -> Plugins::sessionPushPromise
31
31
  | (:retries, ?options) -> Plugins::sessionRetries
32
- | (:rate_limiter, ?options) -> Session
32
+ | (:rate_limiter, ?options) -> Plugins::sessionRateLimiter
33
33
  | (:stream, ?options) -> Plugins::sessionStream
34
34
  | (:stream_bidi, ?options) -> Plugins::sessionStreamBidi
35
35
  | (:aws_sigv4, ?options) -> Plugins::awsSigV4Session
@@ -79,6 +79,8 @@ module HTTPX
79
79
 
80
80
  def on_stream_refuse: (::HTTP2::Stream stream, Request request, StandardError error) -> void
81
81
 
82
+ def on_stream_half_close: (::HTTP2::Stream stream, Request request) -> void
83
+
82
84
  def on_stream_close: (::HTTP2::Stream stream, Request request, (Symbol | StandardError)? error) -> void
83
85
 
84
86
  def on_frame: (string bytes) -> void
@@ -87,13 +89,15 @@ module HTTPX
87
89
 
88
90
  def on_close: (Integer last_frame, Symbol? error, String? payload) -> void
89
91
 
90
- def on_frame_sent: (::HTTP2::frame) -> void
92
+ def on_frame_sent: (::HTTP2::frame frame) -> void
93
+
94
+ def frame_with_extra_info: (::HTTP2::frame frame) -> Hash[Symbol, untyped]
91
95
 
92
- def on_frame_received: (::HTTP2::frame) -> void
96
+ def on_frame_received: (::HTTP2::frame frame) -> void
93
97
 
94
- def on_altsvc: (String origin, ::HTTP2::frame) -> void
98
+ def on_altsvc: (String origin, ::HTTP2::frame frame) -> void
95
99
 
96
- def on_promise: (::HTTP2::Stream) -> void
100
+ def on_promise: (::HTTP2::Stream stream) -> void
97
101
 
98
102
  def on_origin: (String) -> void
99
103
 
data/sig/connection.rbs CHANGED
@@ -37,6 +37,7 @@ module HTTPX
37
37
  @read_buffer: Buffer
38
38
  @write_buffer: Buffer
39
39
  @inflight: Integer
40
+ @max_concurrent_requests: Integer?
40
41
  @keep_alive_timeout: Numeric?
41
42
  @timeout: Numeric?
42
43
  @current_timeout: Numeric?
@@ -119,6 +120,8 @@ module HTTPX
119
120
 
120
121
  def on_error: (HTTPX::TimeoutError | Error | StandardError error, ?Request? request) -> void
121
122
 
123
+ def on_io_error: (IOError error) -> void
124
+
122
125
  private
123
126
 
124
127
  def initialize: (http_uri uri, Options options) -> void
@@ -31,6 +31,8 @@ module HTTPX
31
31
  def cookie_value: () -> String
32
32
  alias to_s cookie_value
33
33
 
34
+ def match?: (String | cookie_attributes name_or_options) -> bool
35
+
34
36
  def valid_for_uri?: (http_uri uri) -> bool
35
37
 
36
38
  def self.new: (Cookie) -> instance
@@ -41,8 +43,7 @@ module HTTPX
41
43
 
42
44
  private
43
45
 
44
- def initialize: (cookie_attributes) -> untyped
45
- | (_ToS, _ToS, ?cookie_attributes) -> untyped
46
+ def initialize: (_ToS name, _ToS value, ?cookie_attributes) -> void
46
47
 
47
48
  def acceptable_from_uri?: (uri) -> bool
48
49
 
@@ -6,11 +6,20 @@ module HTTPX
6
6
  include Enumerable[Cookie]
7
7
 
8
8
  @cookies: Array[Cookie]
9
+ @mtx: Thread::Mutex
9
10
 
10
11
  def parse: (String set_cookie) -> void
11
12
 
13
+ def get: (String | cookie_attributes name_or_options) -> Cookie?
14
+
15
+ def get_all: (String | cookie_attributes name_or_options) -> Array[Cookie]
16
+
17
+ def set: (_ToS | Cookie name, ?(cookie_attributes | _ToS) value_or_options) -> void
18
+
12
19
  def add: (Cookie name, ?String path) -> void
13
20
 
21
+ def delete: (String | Cookie | cookie_attributes name_or_options) -> void
22
+
14
23
  def []: (http_uri) -> Array[Cookie]
15
24
 
16
25
  def each: (?http_uri?) { (Cookie) -> void } -> void
@@ -21,6 +30,8 @@ module HTTPX
21
30
  private
22
31
 
23
32
  def initialize: (?_Each[cookie] cookies) -> untyped
33
+
34
+ def synchronize: [T] { () -> T } -> T
24
35
  end
25
36
  end
26
37
  end
@@ -15,6 +15,8 @@ module HTTPX
15
15
 
16
16
  module InstanceMethods
17
17
  def cookies: () -> Jar
18
+
19
+ def make_jar: (*untyped) -> Jar
18
20
  end
19
21
 
20
22
  module HeadersMethods
@@ -2,14 +2,29 @@ module HTTPX
2
2
  module Plugins
3
3
  module Expect
4
4
  EXPECT_TIMEOUT: Integer
5
+ NOEXPECT_STORE_MUTEX: Thread::Mutex
6
+
7
+ self.@no_expect_store: Store
8
+ def self.no_expect_store: () -> Store
9
+
10
+ def self.extra_options: (Options) -> (Options & _ExpectOptions)
11
+
12
+ class Store
13
+ @store: Array[String]
14
+ @mutex: Thread::Mutex
15
+
16
+ def include?: (String host) -> bool
17
+
18
+ def add: (String host) -> void
19
+
20
+ def delete: (String host) -> void
21
+ end
5
22
 
6
23
  interface _ExpectOptions
7
24
  def expect_timeout: () -> Integer?
8
25
 
9
26
  def expect_threshold_size: () -> Integer?
10
27
  end
11
-
12
- def self.extra_options: (Options) -> (Options & _ExpectOptions)
13
28
  end
14
29
  end
15
30
  end
@@ -5,6 +5,10 @@ module HTTPX
5
5
  module Plugins
6
6
  module Proxy
7
7
  module Socks4
8
+ VERSION: Integer
9
+ CONNECT: Integer
10
+ GRANTED: Integer
11
+ PROTOCOLS: Array[String]
8
12
 
9
13
  module ConnectionMethods
10
14
  def __socks4_proxy_connect: () -> void
@@ -5,11 +5,11 @@ module HTTPX
5
5
 
6
6
  def self.load_dependencies: (singleton(Session)) -> void
7
7
 
8
- def self.retry_after_rate_limit: (untyped, response) -> Numeric?
9
-
10
8
  module InstanceMethods
11
9
  def rate_limit_error?: (response response) -> bool
12
10
  end
13
11
  end
12
+
13
+ type sessionRateLimiter = Session & RateLimiter::InstanceMethods
14
14
  end
15
15
  end
@@ -51,7 +51,7 @@ module HTTPX
51
51
  module ResponseMethods
52
52
  attr_writer original_request: cacheRequest
53
53
 
54
- @cache: bool
54
+ @cached: bool
55
55
  @cache_control: Array[String]?
56
56
  @vary: Array[String]?
57
57
  @date: Time?
@@ -66,9 +66,9 @@ module HTTPX
66
66
 
67
67
  def fresh?: () -> bool
68
68
 
69
- def cache_control: () -> Array[String]?
69
+ %a{pure} def cache_control: () -> Array[String]?
70
70
 
71
- def vary: () -> Array[String]?
71
+ %a{pure} def vary: () -> Array[String]?
72
72
 
73
73
  private
74
74
 
@@ -8,16 +8,16 @@ module HTTPX
8
8
  DEFAULT_JITTER: ^(Numeric) -> Numeric
9
9
  BACKOFF_ALGORITHMS: Array[Symbol]
10
10
 
11
- def self?.retry_after_polynomial_backoff: (retriesRequest request, response response) -> Numeric
11
+ def self?.retry_after_polynomial_backoff: (retriesRequest request, retriesResponse response) -> Numeric
12
12
 
13
- def self?.retry_after_exponential_backoff: (retriesRequest request, response response) -> Numeric
13
+ def self?.retry_after_exponential_backoff: (retriesRequest request, retriesResponse response) -> Numeric
14
14
 
15
15
  interface _RetryCallback
16
- def call: (response response) -> bool?
16
+ def call: (retriesResponse response) -> bool?
17
17
  end
18
18
 
19
19
  interface _RetriesOptions
20
- def retry_after: () -> (^(retriesRequest request, response response) -> Numeric | Numeric)?
20
+ def retry_after: () -> (^(retriesRequest request, retriesResponse response) -> Numeric | Numeric)?
21
21
 
22
22
  def retry_jitter: () -> ^(Numeric jitter) -> Numeric
23
23
 
@@ -35,17 +35,19 @@ module HTTPX
35
35
 
36
36
  private
37
37
 
38
- def fetch_response: (retriesRequest request, Selector selector, retriesOptions options) -> (retriesResponse | ErrorResponse)?
38
+ def fetch_response: (retriesRequest request, Selector selector, retriesOptions options) -> retriesResponse?
39
39
 
40
- def retryable_request?: (retriesRequest request, response response, retriesOptions options) -> boolish
40
+ def retryable_request?: (retriesRequest request, retriesResponse response, retriesOptions options) -> boolish
41
41
 
42
- def retryable_response?: (response response, retriesOptions options) -> boolish
42
+ def retryable_response?: (retriesResponse response, retriesOptions options) -> boolish
43
43
 
44
44
  def retryable_error?: (_Exception error, Options options) -> boolish
45
45
 
46
- def try_partial_retry: (retriesRequest request, (retriesResponse | ErrorResponse) response) -> void
46
+ def try_partial_retry: (retriesRequest request, retriesResponse response) -> void
47
47
 
48
- def prepare_to_retry: (Request & RequestMethods request, response response) -> void
48
+ def prepare_to_retry: (Request & RequestMethods request, retriesResponse response) -> void
49
+
50
+ def when_to_retry: (Request & RequestMethods request, retriesResponse response, retriesOptions options) -> void
49
51
  end
50
52
 
51
53
  module RequestMethods
@@ -53,20 +55,22 @@ module HTTPX
53
55
 
54
56
  attr_accessor retries: Integer
55
57
 
56
- attr_writer partial_response: Response?
58
+ attr_writer partial_response: retriesResponse?
57
59
 
58
- def response=: (retriesResponse | ErrorResponse response) -> void
60
+ def response=: (retriesResponse response) -> void
59
61
  end
60
62
 
61
63
  module ResponseMethods
62
- def from_partial_response: (Response response) -> void
64
+ def from_partial_response: (retriesHTTPResponse response) -> void
63
65
  end
64
66
 
65
67
  type retriesOptions = Options & _RetriesOptions
66
68
 
67
69
  type retriesRequest = Request & RequestMethods
68
70
 
69
- type retriesResponse = Response & ResponseMethods
71
+ type retriesHTTPResponse = Response & ResponseMethods
72
+
73
+ type retriesResponse = retriesHTTPResponse | ErrorResponse
70
74
  end
71
75
 
72
76
  type sessionRetries = Session & Retries::InstanceMethods
data/sig/request.rbs CHANGED
@@ -6,6 +6,7 @@ module HTTPX
6
6
 
7
7
  METHODS: Array[Symbol]
8
8
  USER_AGENT: String
9
+ ALLOWED_URI_SCHEMES: Array[String]
9
10
 
10
11
  attr_reader verb: verb
11
12
  attr_reader uri: http_uri
@@ -37,6 +37,8 @@ module HTTPX
37
37
 
38
38
  def handle_socket_timeout: (Numeric interval) -> void
39
39
 
40
+ def on_io_error: (IOError error) -> void
41
+
40
42
  private
41
43
 
42
44
  def initialize: (ip_family family, options options) -> void
@@ -27,8 +27,6 @@ module HTTPX
27
27
 
28
28
  def terminate: () -> void
29
29
 
30
- def force_close: (*untyped args) -> void
31
-
32
30
  def closed?: () -> bool
33
31
 
34
32
  def empty?: () -> bool
@@ -60,6 +58,8 @@ module HTTPX
60
58
  def resolve_error: (String hostname, ?StandardError?) -> (ResolveError | ResolveTimeoutError)
61
59
 
62
60
  def close_or_resolve: () -> void
61
+
62
+ def disconnect: () -> void
63
63
  end
64
64
  end
65
65
  end
data/sig/selector.rbs CHANGED
@@ -13,6 +13,10 @@ module HTTPX
13
13
  def handle_socket_timeout: (Numeric interval) -> void
14
14
 
15
15
  def on_error: (StandardError) -> void
16
+
17
+ def on_io_error: (IOError error) -> void
18
+
19
+ def force_close: (?bool delete_pending) -> void
16
20
  end
17
21
 
18
22
  class Selector
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: httpx
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.2
4
+ version: 1.7.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tiago Cardoso
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 1.0.0
18
+ version: 1.1.3
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 1.0.0
25
+ version: 1.1.3
26
26
  description: A client library for making HTTP requests from Ruby.
27
27
  email:
28
28
  - cardoso_tiago@hotmail.com
@@ -162,6 +162,7 @@ extra_rdoc_files:
162
162
  - doc/release_notes/1_7_0.md
163
163
  - doc/release_notes/1_7_1.md
164
164
  - doc/release_notes/1_7_2.md
165
+ - doc/release_notes/1_7_3.md
165
166
  files:
166
167
  - LICENSE.txt
167
168
  - README.md
@@ -296,6 +297,7 @@ files:
296
297
  - doc/release_notes/1_7_0.md
297
298
  - doc/release_notes/1_7_1.md
298
299
  - doc/release_notes/1_7_2.md
300
+ - doc/release_notes/1_7_3.md
299
301
  - lib/httpx.rb
300
302
  - lib/httpx/adapters/datadog.rb
301
303
  - lib/httpx/adapters/faraday.rb