httpx 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_7_1.md +21 -0
  3. data/lib/httpx/adapters/webmock.rb +18 -9
  4. data/lib/httpx/altsvc.rb +1 -1
  5. data/lib/httpx/connection/http1.rb +4 -3
  6. data/lib/httpx/connection.rb +4 -1
  7. data/lib/httpx/io/tcp.rb +1 -1
  8. data/lib/httpx/options.rb +78 -5
  9. data/lib/httpx/parser/http1.rb +1 -0
  10. data/lib/httpx/plugins/auth.rb +28 -2
  11. data/lib/httpx/plugins/oauth.rb +11 -18
  12. data/lib/httpx/plugins/persistent.rb +3 -5
  13. data/lib/httpx/plugins/proxy/http.rb +0 -4
  14. data/lib/httpx/plugins/proxy.rb +3 -1
  15. data/lib/httpx/plugins/query.rb +1 -1
  16. data/lib/httpx/plugins/rate_limiter.rb +20 -15
  17. data/lib/httpx/plugins/retries.rb +8 -11
  18. data/lib/httpx/plugins/stream.rb +2 -2
  19. data/lib/httpx/plugins/stream_bidi.rb +13 -1
  20. data/lib/httpx/pool.rb +0 -1
  21. data/lib/httpx/request/body.rb +1 -1
  22. data/lib/httpx/resolver/cache/base.rb +136 -0
  23. data/lib/httpx/resolver/cache/memory.rb +42 -0
  24. data/lib/httpx/resolver/cache.rb +18 -0
  25. data/lib/httpx/resolver/https.rb +7 -3
  26. data/lib/httpx/resolver/multi.rb +7 -3
  27. data/lib/httpx/resolver/native.rb +6 -2
  28. data/lib/httpx/resolver/resolver.rb +1 -1
  29. data/lib/httpx/resolver.rb +3 -149
  30. data/lib/httpx/response/body.rb +3 -3
  31. data/lib/httpx/selector.rb +5 -3
  32. data/lib/httpx/session.rb +1 -1
  33. data/lib/httpx/timers.rb +6 -12
  34. data/lib/httpx/transcoder/gzip.rb +7 -2
  35. data/lib/httpx/transcoder/multipart/decoder.rb +1 -1
  36. data/lib/httpx/transcoder/multipart.rb +1 -1
  37. data/lib/httpx/utils.rb +13 -0
  38. data/lib/httpx/version.rb +1 -1
  39. data/sig/altsvc.rbs +7 -4
  40. data/sig/loggable.rbs +1 -1
  41. data/sig/options.rbs +11 -3
  42. data/sig/plugins/auth.rbs +10 -1
  43. data/sig/plugins/oauth.rbs +0 -2
  44. data/sig/plugins/rate_limiter.rbs +4 -2
  45. data/sig/plugins/retries.rbs +4 -2
  46. data/sig/resolver/cache/base.rbs +28 -0
  47. data/sig/resolver/cache/memory.rbs +13 -0
  48. data/sig/resolver/cache.rbs +16 -0
  49. data/sig/resolver/https.rbs +19 -0
  50. data/sig/resolver/multi.rbs +6 -0
  51. data/sig/resolver.rbs +0 -24
  52. data/sig/timers.rbs +1 -1
  53. data/sig/utils.rbs +2 -0
  54. metadata +9 -1
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resolv"
4
+
5
+ module HTTPX
6
+ module Resolver::Cache
7
+ # Base class of the Resolver Cache adapter implementations.
8
+ #
9
+ # While resolver caches are not required to inherit from this class, it nevertheless provides
10
+ # common useful functions for desired functionality, such as singleton object ractor-safe access,
11
+ # or a default #resolve implementation which deals with IPs and the system hosts file.
12
+ #
13
+ class Base
14
+ MAX_CACHE_SIZE = 512
15
+ CACHE_MUTEX = Thread::Mutex.new
16
+ HOSTS = Resolv::Hosts.new
17
+ @cache = nil
18
+
19
+ class << self
20
+ attr_reader :hosts_resolver
21
+
22
+ # returns the singleton instance to be used within the current ractor.
23
+ def cache(label)
24
+ return Ractor.store_if_absent(:"httpx_resolver_cache_#{label}") { new } if Utils.in_ractor?
25
+
26
+ @cache ||= CACHE_MUTEX.synchronize do
27
+ @cache || new
28
+ end
29
+ end
30
+ end
31
+
32
+ # resolves +hostname+ into an instance of HTTPX::Resolver::Entry if +hostname+ is an IP,
33
+ # or can be found in the cache, or can be found in the system hosts file.
34
+ def resolve(hostname)
35
+ ip_resolve(hostname) || get(hostname) || hosts_resolve(hostname)
36
+ end
37
+
38
+ private
39
+
40
+ # tries to convert +hostname+ into an IPAddr, returns <tt>nil</tt> otherwise.
41
+ def ip_resolve(hostname)
42
+ [Resolver::Entry.new(hostname)]
43
+ rescue ArgumentError
44
+ end
45
+
46
+ # matches +hostname+ to entries in the hosts file, returns <tt>nil</nil> if none is
47
+ # found, or there is no hosts file.
48
+ def hosts_resolve(hostname)
49
+ ips = if Utils.in_ractor?
50
+ Ractor.store_if_absent(:httpx_hosts_resolver) { Resolv::Hosts.new }
51
+ else
52
+ HOSTS
53
+ end.getaddresses(hostname)
54
+
55
+ return if ips.empty?
56
+
57
+ ips.map { |ip| Resolver::Entry.new(ip) }
58
+ rescue IOError
59
+ end
60
+
61
+ # not to be used directly!
62
+ def _get(hostname, lookups, hostnames, ttl)
63
+ return unless lookups.key?(hostname)
64
+
65
+ entries = lookups[hostname]
66
+
67
+ return unless entries
68
+
69
+ entries.delete_if do |address|
70
+ address["TTL"] < ttl
71
+ end
72
+
73
+ if entries.empty?
74
+ lookups.delete(hostname)
75
+ hostnames.delete(hostname)
76
+ end
77
+
78
+ ips = entries.flat_map do |address|
79
+ if (als = address["alias"])
80
+ _get(als, lookups, hostnames, ttl)
81
+ else
82
+ Resolver::Entry.new(address["data"], address["TTL"])
83
+ end
84
+ end.compact
85
+
86
+ ips unless ips.empty?
87
+ end
88
+
89
+ def _set(hostname, family, entries, lookups, hostnames)
90
+ # lru cleanup
91
+ while lookups.size >= MAX_CACHE_SIZE
92
+ hs = hostnames.shift
93
+ lookups.delete(hs)
94
+ end
95
+ hostnames << hostname
96
+
97
+ lookups[hostname] ||= [] # when there's no default proc
98
+
99
+ case family
100
+ when Socket::AF_INET6
101
+ lookups[hostname].concat(entries)
102
+ when Socket::AF_INET
103
+ lookups[hostname].unshift(*entries)
104
+ end
105
+ entries.each do |entry|
106
+ name = entry["name"]
107
+ next unless name != hostname
108
+
109
+ lookups[name] ||= []
110
+
111
+ case family
112
+ when Socket::AF_INET6
113
+ lookups[name] << entry
114
+ when Socket::AF_INET
115
+ lookups[name].unshift(entry)
116
+ end
117
+ end
118
+ end
119
+
120
+ def _evict(hostname, ip, lookups, hostnames)
121
+ return unless lookups.key?(hostname)
122
+
123
+ entries = lookups[hostname]
124
+
125
+ return unless entries
126
+
127
+ entries.delete_if { |entry| entry["data"] == ip }
128
+
129
+ return unless entries.empty?
130
+
131
+ lookups.delete(hostname)
132
+ hostnames.delete(hostname)
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTPX
4
+ module Resolver::Cache
5
+ # Implementation of a thread-safe in-memory LRU resolver cache.
6
+ class Memory < Base
7
+ def initialize
8
+ super
9
+ @hostnames = []
10
+ @lookups = Hash.new { |h, k| h[k] = [] }
11
+ @lookup_mutex = Thread::Mutex.new
12
+ end
13
+
14
+ def get(hostname)
15
+ now = Utils.now
16
+ synchronize do |lookups, hostnames|
17
+ _get(hostname, lookups, hostnames, now)
18
+ end
19
+ end
20
+
21
+ def set(hostname, family, entries)
22
+ synchronize do |lookups, hostnames|
23
+ _set(hostname, family, entries, lookups, hostnames)
24
+ end
25
+ end
26
+
27
+ def evict(hostname, ip)
28
+ ip = ip.to_s
29
+
30
+ synchronize do |lookups, hostnames|
31
+ _evict(hostname, ip, lookups, hostnames)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def synchronize
38
+ @lookup_mutex.synchronize { yield(@lookups, @hostnames) }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httpx/resolver/cache/base"
4
+ require "httpx/resolver/cache/memory"
5
+
6
+ module HTTPX::Resolver
7
+ # The internal resolvers cache adapters are defined under this namespace.
8
+ #
9
+ # Adapters must comply with the Resolver Cache Adapter API and implement the following methods:
10
+ #
11
+ # * #resolve: (String hostname) -> Array[HTTPX::Entry]? => resolves hostname to a list of cached IPs (if found in cache or system)
12
+ # * #get: (String hostname) -> Array[HTTPX::Entry]? => resolves hostname to a list of cached IPs (if found in cache)
13
+ # * #set: (String hostname, Integer ip_family, Array[dns_result]) -> void => stores the set of results in the cache indexes for
14
+ # the hostname and the IP family
15
+ # * #evict: (String hostname, _ToS ip) -> void => evicts the ip for the hostname from the cache (usually done when no longer reachable)
16
+ module Cache
17
+ end
18
+ end
@@ -31,7 +31,7 @@ module HTTPX
31
31
  use_get: false,
32
32
  }.freeze
33
33
 
34
- def_delegators :@resolver_connection, :state, :connecting?, :to_io, :call, :close,
34
+ def_delegators :@resolver_connection, :connecting?, :to_io, :call, :close,
35
35
  :closed?, :deactivate, :terminate, :inflight?, :handle_socket_timeout
36
36
 
37
37
  def initialize(_, options)
@@ -48,10 +48,14 @@ module HTTPX
48
48
  @resolver.lazy_initialize
49
49
  end
50
50
 
51
+ def state
52
+ @resolver_connection ? @resolver_connection.state : :idle
53
+ end
54
+
51
55
  def <<(connection)
52
56
  return if @uri.origin == connection.peer.to_s
53
57
 
54
- @uri_addresses ||= HTTPX::Resolver.nolookup_resolve(@uri.host) || @resolver.getaddresses(@uri.host)
58
+ @uri_addresses ||= @options.resolver_cache.resolve(@uri.host) || @resolver.getaddresses(@uri.host)
55
59
 
56
60
  if @uri_addresses.empty?
57
61
  ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
@@ -247,7 +251,7 @@ module HTTPX
247
251
  # eliminate other candidates
248
252
  @queries.delete_if { |_, conn| connection == conn }
249
253
 
250
- Resolver.cached_lookup_set(hostname, @family, addresses) if @resolver_options[:cache]
254
+ @options.resolver_cache.set(hostname, @family, addresses) if @resolver_options[:cache]
251
255
  catch(:coalesced) { emit_addresses(connection, @family, addresses.map { |a| Resolver::Entry.new(a["data"], a["TTL"]) }) }
252
256
  end
253
257
  end
@@ -19,8 +19,6 @@ module HTTPX
19
19
  resolver.multi = self
20
20
  resolver
21
21
  end
22
-
23
- @errors = Hash.new { |hs, k| hs[k] = [] }
24
22
  end
25
23
 
26
24
  def state
@@ -47,7 +45,7 @@ module HTTPX
47
45
 
48
46
  def early_resolve(connection)
49
47
  hostname = connection.peer.host
50
- addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
48
+ addresses = @resolver_options[:cache] && (connection.addresses || nolookup_resolve(hostname, connection.options))
51
49
  return false unless addresses
52
50
 
53
51
  ip_families = connection.options.ip_families
@@ -83,5 +81,11 @@ module HTTPX
83
81
  @current_session.select_resolver(resolver, @current_selector)
84
82
  end
85
83
  end
84
+
85
+ private
86
+
87
+ def nolookup_resolve(hostname, options)
88
+ options.resolver_cache.resolve(hostname)
89
+ end
86
90
  end
87
91
  end
@@ -136,7 +136,11 @@ module HTTPX
136
136
  private
137
137
 
138
138
  def calculate_interests
139
- return if @queries.empty?
139
+ if @queries.empty?
140
+ return @io.interests if (@socket_type == :tcp) && (@state == :idle)
141
+
142
+ return
143
+ end
140
144
 
141
145
  return :r if @write_buffer.empty?
142
146
 
@@ -417,7 +421,7 @@ module HTTPX
417
421
  reset_hostname(name, connection: connection)
418
422
  @timeouts.delete(connection.peer.host)
419
423
  @connections.delete(connection)
420
- Resolver.cached_lookup_set(connection.peer.host, @family, addresses) if @resolver_options[:cache]
424
+ @options.resolver_cache.set(connection.peer.host, @family, addresses) if @resolver_options[:cache]
421
425
  catch(:coalesced) do
422
426
  emit_addresses(connection, @family, addresses.map { |a| Resolver::Entry.new(a["data"], a["TTL"]) })
423
427
  end
@@ -125,7 +125,7 @@ module HTTPX
125
125
  end
126
126
 
127
127
  def early_resolve(connection, hostname: connection.peer.host) # rubocop:disable Naming/PredicateMethod
128
- addresses = @resolver_options[:cache] && (connection.addresses || HTTPX::Resolver.nolookup_resolve(hostname))
128
+ addresses = @resolver_options[:cache] && (connection.addresses || @options.resolver_cache.resolve(hostname))
129
129
 
130
130
  return false unless addresses
131
131
 
@@ -8,150 +8,27 @@ module HTTPX
8
8
  extend self
9
9
 
10
10
  RESOLVE_TIMEOUT = [2, 3].freeze
11
- MAX_CACHE_SIZE = 512
12
-
13
11
  require "httpx/resolver/entry"
12
+ require "httpx/resolver/cache"
14
13
  require "httpx/resolver/resolver"
15
14
  require "httpx/resolver/system"
16
15
  require "httpx/resolver/native"
17
16
  require "httpx/resolver/https"
18
17
  require "httpx/resolver/multi"
19
18
 
20
- @lookup_mutex = Thread::Mutex.new
21
- @hostnames = []
22
- @lookups = Hash.new { |h, k| h[k] = [] }
23
-
24
19
  @identifier_mutex = Thread::Mutex.new
25
20
  @identifier = 1
26
- @hosts_resolver = Resolv::Hosts.new
27
21
 
28
22
  def supported_ip_families
29
- if in_ractor?
23
+ if Utils.in_ractor?
30
24
  Ractor.store_if_absent(:httpx_supported_ip_families) { find_supported_ip_families }
31
25
  else
32
26
  @supported_ip_families ||= find_supported_ip_families
33
27
  end
34
28
  end
35
29
 
36
- def resolver_for(resolver_type, options)
37
- case resolver_type
38
- when Symbol
39
- meth = :"resolver_#{resolver_type}_class"
40
-
41
- return options.__send__(meth) if options.respond_to?(meth)
42
- when Class
43
- return resolver_type if resolver_type < Resolver
44
- end
45
-
46
- raise Error, "unsupported resolver type (#{resolver_type})"
47
- end
48
-
49
- def nolookup_resolve(hostname)
50
- ip_resolve(hostname) || cached_lookup(hostname) || hosts_resolve(hostname)
51
- end
52
-
53
- # tries to convert +hostname+ into an IPAddr, returns <tt>nil</tt> otherwise.
54
- def ip_resolve(hostname)
55
- [Entry.new(hostname)]
56
- rescue ArgumentError
57
- end
58
-
59
- # matches +hostname+ to entries in the hosts file, returns <tt>nil</nil> if none is
60
- # found, or there is no hosts file.
61
- def hosts_resolve(hostname)
62
- ips = if in_ractor?
63
- Ractor.store_if_absent(:httpx_hosts_resolver) { Resolv::Hosts.new }
64
- else
65
- @hosts_resolver
66
- end.getaddresses(hostname)
67
-
68
- return if ips.empty?
69
-
70
- ips.map { |ip| Entry.new(ip) }
71
- rescue IOError
72
- end
73
-
74
- def cached_lookup(hostname)
75
- now = Utils.now
76
- lookup_synchronize do |lookups, hostnames|
77
- lookup(hostname, lookups, hostnames, now)
78
- end
79
- end
80
-
81
- def cached_lookup_set(hostname, family, entries)
82
- lookup_synchronize do |lookups, hostnames|
83
- # lru cleanup
84
- while lookups.size >= MAX_CACHE_SIZE
85
- hs = hostnames.shift
86
- lookups.delete(hs)
87
- end
88
- hostnames << hostname
89
-
90
- case family
91
- when Socket::AF_INET6
92
- lookups[hostname].concat(entries)
93
- when Socket::AF_INET
94
- lookups[hostname].unshift(*entries)
95
- end
96
- entries.each do |entry|
97
- name = entry["name"]
98
- next unless name != hostname
99
-
100
- case family
101
- when Socket::AF_INET6
102
- lookups[name] << entry
103
- when Socket::AF_INET
104
- lookups[name].unshift(entry)
105
- end
106
- end
107
- end
108
- end
109
-
110
- def cached_lookup_evict(hostname, ip)
111
- ip = ip.to_s
112
-
113
- lookup_synchronize do |lookups, hostnames|
114
- entries = lookups[hostname]
115
-
116
- return unless entries
117
-
118
- entries.delete_if { |entry| entry["data"] == ip }
119
-
120
- if entries.empty?
121
- lookups.delete(hostname)
122
- hostnames.delete(hostname)
123
- end
124
- end
125
- end
126
-
127
- # do not use directly!
128
- def lookup(hostname, lookups, hostnames, ttl)
129
- return unless lookups.key?(hostname)
130
-
131
- entries = lookups[hostname]
132
-
133
- entries.delete_if do |address|
134
- address["TTL"] < ttl
135
- end
136
-
137
- if entries.empty?
138
- lookups.delete(hostname)
139
- hostnames.delete(hostname)
140
- end
141
-
142
- ips = entries.flat_map do |address|
143
- if (als = address["alias"])
144
- lookup(als, lookups, hostnames, ttl)
145
- else
146
- Entry.new(address["data"], address["TTL"])
147
- end
148
- end.compact
149
-
150
- ips unless ips.empty?
151
- end
152
-
153
30
  def generate_id
154
- if in_ractor?
31
+ if Utils.in_ractor?
155
32
  identifier = Ractor.store_if_absent(:httpx_resolver_identifier) { -1 }
156
33
  Ractor.current[:httpx_resolver_identifier] = (identifier + 1) & 0xFFFF
157
34
  else
@@ -213,16 +90,6 @@ module HTTPX
213
90
 
214
91
  private
215
92
 
216
- def lookup_synchronize
217
- if in_ractor?
218
- lookups = Ractor.store_if_absent(:httpx_resolver_lookups) { Hash.new { |h, k| h[k] = [] } }
219
- hostnames = Ractor.store_if_absent(:httpx_resolver_hostnames) { [] }
220
- return yield(lookups, hostnames)
221
- end
222
-
223
- @lookup_mutex.synchronize { yield(@lookups, @hostnames) }
224
- end
225
-
226
93
  def id_synchronize(&block)
227
94
  @identifier_mutex.synchronize(&block)
228
95
  end
@@ -240,18 +107,5 @@ module HTTPX
240
107
  [Socket::AF_INET]
241
108
  end.freeze
242
109
  end
243
-
244
- if defined?(Ractor) &&
245
- # no ractor support for 3.0
246
- RUBY_VERSION >= "3.1.0"
247
-
248
- def in_ractor?
249
- Ractor.main != Ractor.current
250
- end
251
- else
252
- def in_ractor?
253
- false
254
- end
255
- end
256
110
  end
257
111
  end
@@ -59,13 +59,11 @@ module HTTPX
59
59
 
60
60
  chunk = decode_chunk(chunk)
61
61
 
62
- size = chunk.bytesize
63
- @length += size
64
62
  transition(:open)
65
63
  @buffer.write(chunk)
66
64
 
67
65
  @response.emit(:chunk_received, chunk)
68
- size
66
+ chunk.bytesize
69
67
  end
70
68
 
71
69
  # reads a chunk from the payload (implementation of the IO reader protocol).
@@ -209,6 +207,8 @@ module HTTPX
209
207
  chunk = inflater.call(chunk)
210
208
  end if @inflaters
211
209
 
210
+ @length += chunk.bytesize
211
+
212
212
  chunk
213
213
  end
214
214
 
@@ -127,13 +127,15 @@ module HTTPX
127
127
  private
128
128
 
129
129
  def select(interval, &block)
130
- has_no_selectables = @selectables.empty?
131
130
  # do not cause an infinite loop here.
132
131
  #
133
132
  # this may happen if timeout calculation actually triggered an error which causes
134
133
  # the connections to be reaped (such as the total timeout error) before #select
135
134
  # gets called.
136
- return if interval.nil? && has_no_selectables
135
+ if @selectables.empty?
136
+ sleep(interval) if interval
137
+ return
138
+ end
137
139
 
138
140
  # @type var r: (selectable | Array[selectable])?
139
141
  # @type var w: (selectable | Array[selectable])?
@@ -171,7 +173,7 @@ module HTTPX
171
173
  when Array
172
174
  select_many(r, w, interval, &block)
173
175
  when nil
174
- return unless interval && has_no_selectables
176
+ return unless interval && @selectables.any?
175
177
 
176
178
  # no selectables
177
179
  # TODO: replace with sleep?
data/lib/httpx/session.rb CHANGED
@@ -177,7 +177,7 @@ module HTTPX
177
177
 
178
178
  # returns the HTTPX::Connection through which the +request+ should be sent through.
179
179
  def find_connection(request_uri, selector, options)
180
- log(level: 2) { "finding connection for ##{request_uri}..." }
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
183
  connection.log(level: 2) { "found connection##{connection.object_id}(#{connection.state}) in selector##{selector.object_id}" }
data/lib/httpx/timers.rb CHANGED
@@ -29,13 +29,15 @@ module HTTPX
29
29
  end
30
30
 
31
31
  def wait_interval
32
- drop_elapsed!
33
-
34
32
  return if @intervals.empty?
35
33
 
34
+ first_interval = @intervals.first
35
+
36
+ drop_elapsed!(0) if first_interval.elapsed?(0)
37
+
36
38
  @next_interval_at = Utils.now
37
39
 
38
- @intervals.first.interval
40
+ first_interval.interval
39
41
  end
40
42
 
41
43
  def fire(error = nil)
@@ -46,20 +48,12 @@ module HTTPX
46
48
 
47
49
  drop_elapsed!(elapsed_time)
48
50
 
49
- @intervals = @intervals.drop_while { |interval| interval.elapse(elapsed_time) <= 0 }
50
-
51
51
  @next_interval_at = nil if @intervals.empty?
52
52
  end
53
53
 
54
54
  private
55
55
 
56
- def drop_elapsed!(elapsed_time = 0)
57
- # check first, if not elapsed, then return
58
- first_interval = @intervals.first
59
-
60
- return unless first_interval && first_interval.elapsed?(elapsed_time)
61
-
62
- # TODO: would be nice to have a drop_while!
56
+ def drop_elapsed!(elapsed_time)
63
57
  @intervals = @intervals.drop_while { |interval| interval.elapse(elapsed_time) <= 0 }
64
58
  end
65
59
 
@@ -8,6 +8,7 @@ module HTTPX
8
8
  class Deflater < Transcoder::Deflater
9
9
  def initialize(body)
10
10
  @compressed_chunk = "".b
11
+ @deflater = nil
11
12
  super
12
13
  end
13
14
 
@@ -28,8 +29,12 @@ module HTTPX
28
29
 
29
30
  private
30
31
 
31
- def write(chunk)
32
- @compressed_chunk << chunk
32
+ def write(*chunks)
33
+ chunks.sum do |chunk|
34
+ chunk = chunk.to_s
35
+ @compressed_chunk << chunk
36
+ chunk.bytesize
37
+ end
33
38
  end
34
39
 
35
40
  def compressed_chunk
@@ -12,7 +12,6 @@ module HTTPX
12
12
  def initialize(filename, content_type)
13
13
  @original_filename = filename
14
14
  @content_type = content_type
15
- @current = nil
16
15
  @file = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
17
16
  super(@file)
18
17
  end
@@ -39,6 +38,7 @@ module HTTPX
39
38
  @parts = {}
40
39
  @intermediate_boundary = "--#{@boundary}"
41
40
  @state = :idle
41
+ @current = nil
42
42
  end
43
43
 
44
44
  def call(response, *)
@@ -19,7 +19,7 @@ module HTTPX::Transcoder
19
19
 
20
20
  def multipart_value?(value)
21
21
  value.respond_to?(:read) ||
22
- (value.respond_to?(:to_hash) &&
22
+ (value.is_a?(Hash) &&
23
23
  value.key?(:body) &&
24
24
  (value.key?(:filename) || value.key?(:content_type)))
25
25
  end
data/lib/httpx/utils.rb CHANGED
@@ -71,5 +71,18 @@ module HTTPX
71
71
  uri.non_ascii_hostname = non_ascii_hostname
72
72
  uri
73
73
  end
74
+
75
+ if defined?(Ractor) &&
76
+ # no ractor support for 3.0
77
+ RUBY_VERSION >= "3.1.0"
78
+
79
+ def in_ractor?
80
+ Ractor.main != Ractor.current
81
+ end
82
+ else
83
+ def in_ractor?
84
+ false
85
+ end
86
+ end
74
87
  end
75
88
  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.0"
4
+ VERSION = "1.7.1"
5
5
  end
data/sig/altsvc.rbs CHANGED
@@ -5,19 +5,22 @@ module HTTPX
5
5
 
6
6
  def send: (Request request) -> void
7
7
 
8
- def match?: (URI::Generic uri, Options options) -> bool
8
+ def match?: (http_uri uri, Options options) -> bool
9
9
 
10
10
  private
11
11
 
12
- def match_altsvcs?: (URI::Generic uri) -> bool
12
+ def match_altsvcs?: (http_uri uri) -> bool
13
13
 
14
- def match_altsvc_options?: (URI::Generic uri, Options options) -> bool
14
+ def match_altsvc_options?: (http_uri uri, Options options) -> bool
15
15
 
16
- def altsvc_match?: (uri uri, uri other_uri) -> bool
16
+ def altsvc_match?: (http_uri uri, uri other_uri) -> bool
17
17
  end
18
18
 
19
19
  type altsvc_params = Hash[String, untyped]
20
20
 
21
+ self.@altsvc_mutex: Thread::Mutex
22
+ self.@altsvcs: Hash[String, Array[altsvc_params]]
23
+
21
24
  def self?.cached_altsvc: (String origin) -> Array[altsvc_params]
22
25
 
23
26
  def self?.cached_altsvc_set: (String origin, altsvc_params) -> void