httpx 1.6.2 → 1.7.0

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_11_0.md +3 -3
  3. data/doc/release_notes/1_6_3.md +47 -0
  4. data/doc/release_notes/1_7_0.md +149 -0
  5. data/lib/httpx/adapters/datadog.rb +1 -1
  6. data/lib/httpx/adapters/faraday.rb +1 -1
  7. data/lib/httpx/adapters/sentry.rb +1 -1
  8. data/lib/httpx/altsvc.rb +3 -1
  9. data/lib/httpx/connection/http1.rb +14 -15
  10. data/lib/httpx/connection/http2.rb +16 -15
  11. data/lib/httpx/connection.rb +118 -110
  12. data/lib/httpx/domain_name.rb +1 -1
  13. data/lib/httpx/extensions.rb +0 -14
  14. data/lib/httpx/headers.rb +2 -2
  15. data/lib/httpx/io/ssl.rb +1 -1
  16. data/lib/httpx/loggable.rb +14 -2
  17. data/lib/httpx/options.rb +60 -17
  18. data/lib/httpx/plugins/auth/digest.rb +44 -4
  19. data/lib/httpx/plugins/auth.rb +87 -4
  20. data/lib/httpx/plugins/aws_sdk_authentication.rb +0 -1
  21. data/lib/httpx/plugins/callbacks.rb +15 -1
  22. data/lib/httpx/plugins/cookies/cookie.rb +1 -0
  23. data/lib/httpx/plugins/digest_auth.rb +4 -5
  24. data/lib/httpx/plugins/fiber_concurrency.rb +16 -1
  25. data/lib/httpx/plugins/grpc/grpc_encoding.rb +1 -1
  26. data/lib/httpx/plugins/grpc.rb +2 -2
  27. data/lib/httpx/plugins/internal_telemetry.rb +1 -1
  28. data/lib/httpx/plugins/ntlm_auth.rb +5 -3
  29. data/lib/httpx/plugins/oauth.rb +162 -56
  30. data/lib/httpx/plugins/proxy/http.rb +37 -9
  31. data/lib/httpx/plugins/rate_limiter.rb +2 -2
  32. data/lib/httpx/plugins/response_cache/file_store.rb +1 -0
  33. data/lib/httpx/plugins/response_cache.rb +16 -9
  34. data/lib/httpx/plugins/retries.rb +55 -16
  35. data/lib/httpx/plugins/ssrf_filter.rb +1 -1
  36. data/lib/httpx/plugins/stream.rb +59 -8
  37. data/lib/httpx/plugins/stream_bidi.rb +87 -22
  38. data/lib/httpx/pool.rb +65 -21
  39. data/lib/httpx/request.rb +13 -14
  40. data/lib/httpx/resolver/https.rb +100 -34
  41. data/lib/httpx/resolver/multi.rb +12 -27
  42. data/lib/httpx/resolver/native.rb +68 -38
  43. data/lib/httpx/resolver/resolver.rb +46 -29
  44. data/lib/httpx/resolver/system.rb +63 -39
  45. data/lib/httpx/resolver.rb +97 -29
  46. data/lib/httpx/response/body.rb +2 -0
  47. data/lib/httpx/response.rb +22 -6
  48. data/lib/httpx/selector.rb +44 -20
  49. data/lib/httpx/session.rb +23 -33
  50. data/lib/httpx/transcoder/body.rb +1 -1
  51. data/lib/httpx/transcoder/deflate.rb +13 -8
  52. data/lib/httpx/transcoder/json.rb +1 -1
  53. data/lib/httpx/transcoder/multipart/decoder.rb +4 -4
  54. data/lib/httpx/transcoder/multipart/encoder.rb +1 -1
  55. data/lib/httpx/transcoder/multipart.rb +16 -8
  56. data/lib/httpx/transcoder/utils/body_reader.rb +1 -2
  57. data/lib/httpx/transcoder/utils/deflater.rb +1 -2
  58. data/lib/httpx/transcoder.rb +4 -6
  59. data/lib/httpx/version.rb +1 -1
  60. data/sig/altsvc.rbs +3 -0
  61. data/sig/chainable.rbs +3 -3
  62. data/sig/connection.rbs +13 -6
  63. data/sig/loggable.rbs +5 -1
  64. data/sig/options.rbs +6 -2
  65. data/sig/plugins/auth/digest.rbs +6 -0
  66. data/sig/plugins/auth.rbs +28 -4
  67. data/sig/plugins/basic_auth.rbs +3 -3
  68. data/sig/plugins/callbacks.rbs +3 -0
  69. data/sig/plugins/digest_auth.rbs +2 -4
  70. data/sig/plugins/fiber_concurrency.rbs +6 -0
  71. data/sig/plugins/ntlm_auth.rbs +2 -2
  72. data/sig/plugins/oauth.rbs +46 -15
  73. data/sig/plugins/rate_limiter.rbs +1 -1
  74. data/sig/plugins/response_cache/file_store.rbs +2 -0
  75. data/sig/plugins/response_cache.rbs +4 -0
  76. data/sig/plugins/retries.rbs +8 -2
  77. data/sig/plugins/stream.rbs +13 -3
  78. data/sig/plugins/stream_bidi.rbs +5 -7
  79. data/sig/pool.rbs +1 -1
  80. data/sig/resolver/https.rbs +7 -0
  81. data/sig/resolver/multi.rbs +2 -9
  82. data/sig/resolver/native.rbs +1 -1
  83. data/sig/resolver/resolver.rbs +9 -8
  84. data/sig/resolver/system.rbs +4 -2
  85. data/sig/resolver.rbs +12 -3
  86. data/sig/response.rbs +3 -0
  87. data/sig/selector.rbs +2 -0
  88. data/sig/session.rbs +8 -8
  89. data/sig/transcoder/multipart.rbs +4 -2
  90. data/sig/transcoder.rbs +5 -1
  91. metadata +5 -1
@@ -5,7 +5,10 @@ require "resolv"
5
5
 
6
6
  module HTTPX
7
7
  module Resolver
8
+ extend self
9
+
8
10
  RESOLVE_TIMEOUT = [2, 3].freeze
11
+ MAX_CACHE_SIZE = 512
9
12
 
10
13
  require "httpx/resolver/entry"
11
14
  require "httpx/resolver/resolver"
@@ -15,26 +18,19 @@ module HTTPX
15
18
  require "httpx/resolver/multi"
16
19
 
17
20
  @lookup_mutex = Thread::Mutex.new
21
+ @hostnames = []
18
22
  @lookups = Hash.new { |h, k| h[k] = [] }
19
23
 
20
24
  @identifier_mutex = Thread::Mutex.new
21
25
  @identifier = 1
22
26
  @hosts_resolver = Resolv::Hosts.new
23
27
 
24
- module_function
25
-
26
28
  def supported_ip_families
27
- @supported_ip_families ||= begin
28
- # https://github.com/ruby/resolv/blob/095f1c003f6073730500f02acbdbc55f83d70987/lib/resolv.rb#L408
29
- list = Socket.ip_address_list
30
- if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
31
- [Socket::AF_INET6, Socket::AF_INET]
32
- else
33
- [Socket::AF_INET]
34
- end
35
- rescue NotImplementedError
36
- [Socket::AF_INET]
37
- end.freeze
29
+ if in_ractor?
30
+ Ractor.store_if_absent(:httpx_supported_ip_families) { find_supported_ip_families }
31
+ else
32
+ @supported_ip_families ||= find_supported_ip_families
33
+ end
38
34
  end
39
35
 
40
36
  def resolver_for(resolver_type, options)
@@ -63,7 +59,12 @@ module HTTPX
63
59
  # matches +hostname+ to entries in the hosts file, returns <tt>nil</nil> if none is
64
60
  # found, or there is no hosts file.
65
61
  def hosts_resolve(hostname)
66
- ips = @hosts_resolver.getaddresses(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
+
67
68
  return if ips.empty?
68
69
 
69
70
  ips.map { |ip| Entry.new(ip) }
@@ -72,13 +73,20 @@ module HTTPX
72
73
 
73
74
  def cached_lookup(hostname)
74
75
  now = Utils.now
75
- lookup_synchronize do |lookups|
76
- lookup(hostname, lookups, now)
76
+ lookup_synchronize do |lookups, hostnames|
77
+ lookup(hostname, lookups, hostnames, now)
77
78
  end
78
79
  end
79
80
 
80
81
  def cached_lookup_set(hostname, family, entries)
81
- lookup_synchronize do |lookups|
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
+
82
90
  case family
83
91
  when Socket::AF_INET6
84
92
  lookups[hostname].concat(entries)
@@ -86,13 +94,14 @@ module HTTPX
86
94
  lookups[hostname].unshift(*entries)
87
95
  end
88
96
  entries.each do |entry|
89
- next unless entry["name"] != hostname
97
+ name = entry["name"]
98
+ next unless name != hostname
90
99
 
91
100
  case family
92
101
  when Socket::AF_INET6
93
- lookups[entry["name"]] << entry
102
+ lookups[name] << entry
94
103
  when Socket::AF_INET
95
- lookups[entry["name"]].unshift(entry)
104
+ lookups[name].unshift(entry)
96
105
  end
97
106
  end
98
107
  end
@@ -101,26 +110,38 @@ module HTTPX
101
110
  def cached_lookup_evict(hostname, ip)
102
111
  ip = ip.to_s
103
112
 
104
- lookup_synchronize do |lookups|
113
+ lookup_synchronize do |lookups, hostnames|
105
114
  entries = lookups[hostname]
106
115
 
107
116
  return unless entries
108
117
 
109
- lookups.delete_if { |entry| entry["data"] == ip }
118
+ entries.delete_if { |entry| entry["data"] == ip }
119
+
120
+ if entries.empty?
121
+ lookups.delete(hostname)
122
+ hostnames.delete(hostname)
123
+ end
110
124
  end
111
125
  end
112
126
 
113
127
  # do not use directly!
114
- def lookup(hostname, lookups, ttl)
128
+ def lookup(hostname, lookups, hostnames, ttl)
115
129
  return unless lookups.key?(hostname)
116
130
 
117
- entries = lookups[hostname] = lookups[hostname].select do |address|
118
- address["TTL"] > ttl
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)
119
140
  end
120
141
 
121
142
  ips = entries.flat_map do |address|
122
143
  if (als = address["alias"])
123
- lookup(als, lookups, ttl)
144
+ lookup(als, lookups, hostnames, ttl)
124
145
  else
125
146
  Entry.new(address["data"], address["TTL"])
126
147
  end
@@ -130,7 +151,12 @@ module HTTPX
130
151
  end
131
152
 
132
153
  def generate_id
133
- id_synchronize { @identifier = (@identifier + 1) & 0xFFFF }
154
+ if in_ractor?
155
+ identifier = Ractor.store_if_absent(:httpx_resolver_identifier) { -1 }
156
+ Ractor.current[:httpx_resolver_identifier] = (identifier + 1) & 0xFFFF
157
+ else
158
+ id_synchronize { @identifier = (@identifier + 1) & 0xFFFF }
159
+ end
134
160
  end
135
161
 
136
162
  def encode_dns_query(hostname, type: Resolv::DNS::Resource::IN::A, message_id: generate_id)
@@ -152,7 +178,14 @@ module HTTPX
152
178
 
153
179
  return :message_truncated if message.tc == 1
154
180
 
155
- return :dns_error, message.rcode if message.rcode != Resolv::DNS::RCode::NoError
181
+ if message.rcode != Resolv::DNS::RCode::NoError
182
+ case message.rcode
183
+ when Resolv::DNS::RCode::ServFail
184
+ return :retriable_error, message.rcode
185
+ else
186
+ return :dns_error, message.rcode
187
+ end
188
+ end
156
189
 
157
190
  addresses = []
158
191
 
@@ -178,12 +211,47 @@ module HTTPX
178
211
  [:ok, addresses]
179
212
  end
180
213
 
214
+ private
215
+
181
216
  def lookup_synchronize
182
- @lookup_mutex.synchronize { yield(@lookups) }
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) }
183
224
  end
184
225
 
185
226
  def id_synchronize(&block)
186
227
  @identifier_mutex.synchronize(&block)
187
228
  end
229
+
230
+ def find_supported_ip_families
231
+ list = Socket.ip_address_list
232
+
233
+ begin
234
+ if list.any? { |a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
235
+ [Socket::AF_INET6, Socket::AF_INET]
236
+ else
237
+ [Socket::AF_INET]
238
+ end
239
+ rescue NotImplementedError
240
+ [Socket::AF_INET]
241
+ end.freeze
242
+ 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
188
256
  end
189
257
  end
@@ -138,6 +138,8 @@ module HTTPX
138
138
  else
139
139
  IO.copy_stream(@buffer, dest)
140
140
  end
141
+ ensure
142
+ close
141
143
  end
142
144
 
143
145
  # closes/cleans the buffer, resets everything
@@ -211,16 +211,19 @@ module HTTPX
211
211
 
212
212
  def initialize(header_value)
213
213
  @header_value = header_value
214
+ @mime_type = @charset = nil
215
+ @initialized = false
214
216
  end
215
217
 
216
218
  # returns the mime type declared in the header.
217
219
  #
218
220
  # ContentType.new("application/json; charset=utf-8").mime_type #=> "application/json"
219
221
  def mime_type
220
- return @mime_type if defined?(@mime_type)
222
+ return @mime_type if @initialized
221
223
 
222
- m = @header_value.to_s[MIME_TYPE_RE, 1]
223
- m && @mime_type = m.strip.downcase
224
+ load
225
+
226
+ @mime_type
224
227
  end
225
228
 
226
229
  # returns the charset declared in the header.
@@ -228,10 +231,23 @@ module HTTPX
228
231
  # ContentType.new("application/json; charset=utf-8").charset #=> "utf-8"
229
232
  # ContentType.new("text/plain").charset #=> nil
230
233
  def charset
231
- return @charset if defined?(@charset)
234
+ return @charset if @initialized
235
+
236
+ load
237
+
238
+ @charset
239
+ end
240
+
241
+ private
242
+
243
+ def load
244
+ m = @header_value.to_s[MIME_TYPE_RE, 1]
245
+ m && @mime_type = m.strip.downcase
246
+
247
+ c = @header_value.to_s[CHARSET_RE, 1]
248
+ c && @charset = c.strip.delete('"')
232
249
 
233
- m = @header_value.to_s[CHARSET_RE, 1]
234
- m && @charset = m.strip.delete('"')
250
+ @initialized = true
235
251
  end
236
252
  end
237
253
 
@@ -51,7 +51,7 @@ module HTTPX
51
51
 
52
52
  begin
53
53
  select(timeout) do |c|
54
- c.log(level: 2) { "[#{c.state}] selected#{" after #{timeout} secs" unless timeout.nil?}..." }
54
+ c.log(level: 2) { "[#{c.state}] selected from selector##{object_id} #{" after #{timeout} secs" unless timeout.nil?}..." }
55
55
 
56
56
  c.call
57
57
  end
@@ -61,17 +61,6 @@ module HTTPX
61
61
  @timers.fire(e)
62
62
  end
63
63
  end
64
- rescue StandardError => e
65
- each_connection do |c|
66
- c.emit(:error, e)
67
- end
68
- rescue Exception # rubocop:disable Lint/RescueException
69
- each_connection do |conn|
70
- conn.force_reset
71
- conn.disconnect
72
- end
73
-
74
- raise
75
64
  end
76
65
 
77
66
  def terminate
@@ -157,7 +146,9 @@ module HTTPX
157
146
 
158
147
  next(is_closed) if is_closed
159
148
 
160
- io.log(level: 2) { "[#{io.state}] registering for select (#{interests})#{" for #{interval} seconds" unless interval.nil?}" }
149
+ io.log(level: 2) do
150
+ "[#{io.state}] registering in selector##{object_id} for select (#{interests})#{" for #{interval} seconds" unless interval.nil?}"
151
+ end
161
152
 
162
153
  if READABLE.include?(interests)
163
154
  r = r.nil? ? io : (Array(r) << io)
@@ -206,7 +197,27 @@ module HTTPX
206
197
  end
207
198
 
208
199
  def select_many(r, w, interval, &block)
209
- readers, writers = ::IO.select(r, w, nil, interval)
200
+ begin
201
+ readers, writers = ::IO.select(r, w, nil, interval)
202
+ rescue IOError => e
203
+ (Array(r) + Array(w)).each do |sel|
204
+ # TODO: is there a way to cheaply find the IO associated with the error?
205
+ sel.on_error(e)
206
+ sel.force_close(true)
207
+ end
208
+ rescue StandardError => e
209
+ (Array(r) + Array(w)).each do |sel|
210
+ sel.on_error(e)
211
+ end
212
+
213
+ return
214
+ rescue Exception => e # rubocop:disable Lint/RescueException
215
+ (Array(r) + Array(w)).each do |sel|
216
+ sel.force_close(true)
217
+ end
218
+
219
+ raise e
220
+ end
210
221
 
211
222
  if readers.nil? && writers.nil? && interval
212
223
  [*r, *w].each { |io| io.handle_socket_timeout(interval) }
@@ -228,12 +239,25 @@ module HTTPX
228
239
  end
229
240
 
230
241
  def select_one(io, interests, interval)
231
- result =
232
- case interests
233
- when :r then io.to_io.wait_readable(interval)
234
- when :w then io.to_io.wait_writable(interval)
235
- when :rw then rw_wait(io, interval)
236
- end
242
+ begin
243
+ result =
244
+ case interests
245
+ when :r then io.to_io.wait_readable(interval)
246
+ when :w then io.to_io.wait_writable(interval)
247
+ when :rw then rw_wait(io, interval)
248
+ end
249
+ rescue IOError => e
250
+ io.on_error(e)
251
+ io.force_close(true)
252
+ rescue StandardError => e
253
+ io.on_error(e)
254
+
255
+ return
256
+ rescue Exception => e # rubocop:disable Lint/RescueException
257
+ io.force_close(true)
258
+
259
+ raise e
260
+ end
237
261
 
238
262
  unless result || interval.nil?
239
263
  io.handle_socket_timeout(interval) unless @is_timer_interval
data/lib/httpx/session.rb CHANGED
@@ -120,13 +120,16 @@ module HTTPX
120
120
  end
121
121
 
122
122
  def select_connection(connection, selector)
123
- pin_connection(connection, selector)
123
+ pin(connection, selector)
124
+ connection.log(level: 2) do
125
+ "registering into selector##{selector.object_id}"
126
+ end
124
127
  selector.register(connection)
125
128
  end
126
129
 
127
- def pin_connection(connection, selector)
128
- connection.current_session = self
129
- connection.current_selector = selector
130
+ def pin(conn_or_resolver, selector)
131
+ conn_or_resolver.current_session = self
132
+ conn_or_resolver.current_selector = selector
130
133
  end
131
134
 
132
135
  alias_method :select_resolver, :select_connection
@@ -137,9 +140,6 @@ module HTTPX
137
140
  end
138
141
  selector.deregister(connection)
139
142
 
140
- # when connections coalesce
141
- return if connection.state == :idle
142
-
143
143
  return if cloned
144
144
 
145
145
  return if @closing && connection.state == :closed
@@ -177,6 +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
181
  if (connection = selector.find_connection(request_uri, options))
181
182
  connection.idling if connection.state == :closed
182
183
  connection.log(level: 2) { "found connection##{connection.object_id}(#{connection.state}) in selector##{selector.object_id}" }
@@ -194,7 +195,7 @@ module HTTPX
194
195
  if options.io
195
196
  select_connection(connection, selector)
196
197
  else
197
- pin_connection(connection, selector)
198
+ pin(connection, selector)
198
199
  end
199
200
  when :closing, :closed
200
201
  connection.idling
@@ -205,7 +206,7 @@ module HTTPX
205
206
  resolve_connection(connection, selector)
206
207
  end
207
208
  else
208
- pin_connection(connection, selector)
209
+ pin(connection, selector)
209
210
  end
210
211
 
211
212
  connection
@@ -224,13 +225,7 @@ module HTTPX
224
225
 
225
226
  # tries deactivating connections in the +selector+, deregistering the ones that have been deactivated.
226
227
  def deactivate(selector)
227
- selector.each_connection.select do |c|
228
- c.deactivate
229
-
230
- c.state == :inactive
231
- end.each do |c| # rubocop:disable Style/MultilineBlockChain
232
- deselect_connection(c, selector)
233
- end
228
+ selector.each_connection.to_a.each(&:deactivate)
234
229
  end
235
230
 
236
231
  # callback executed when an HTTP/2 promise frame has been received.
@@ -395,6 +390,7 @@ module HTTPX
395
390
 
396
391
  resolver = find_resolver_for(connection, selector)
397
392
 
393
+ pin(connection, selector)
398
394
  resolver.early_resolve(connection) || resolver.lazy_resolve(connection)
399
395
  end
400
396
 
@@ -402,6 +398,9 @@ module HTTPX
402
398
  from_pool = false
403
399
  found_connection = selector.find_mergeable_connection(connection) || begin
404
400
  from_pool = true
401
+ connection.log(level: 2) do
402
+ "try finding a mergeable connection in pool##{@pool.object_id}"
403
+ end
405
404
  @pool.checkout_mergeable_connection(connection)
406
405
  end
407
406
 
@@ -409,29 +408,21 @@ module HTTPX
409
408
 
410
409
  connection.log(level: 2) do
411
410
  "try coalescing from #{from_pool ? "pool##{@pool.object_id}" : "selector##{selector.object_id}"} " \
412
- "(conn##{found_connection.object_id}[#{found_connection.origin}])"
411
+ "(connection##{found_connection.object_id}[#{found_connection.origin}])"
413
412
  end
414
413
 
415
414
  coalesce_connections(found_connection, connection, selector, from_pool)
416
415
  end
417
416
 
418
- def on_resolver_close(resolver, selector)
419
- return if resolver.closed?
420
-
421
- deselect_resolver(resolver, selector)
422
- resolver.close unless resolver.closed?
423
- end
424
-
425
417
  def find_resolver_for(connection, selector)
426
418
  if (resolver = selector.find_resolver(connection.options))
427
- resolver.log(level: 2) { "found resolver##{connection.object_id}(#{connection.state}) in selector##{selector.object_id}" }
419
+ resolver.log(level: 2) { "found resolver##{resolver.object_id}(#{resolver.state}) in selector##{selector.object_id}" }
428
420
  return resolver
429
421
  end
430
422
 
431
423
  resolver = @pool.checkout_resolver(connection.options)
432
- resolver.log(level: 2) { "found resolver##{connection.object_id}(#{connection.state}) in pool##{@pool.object_id}" }
433
- resolver.current_session = self
434
- resolver.current_selector = selector
424
+ resolver.log(level: 2) { "found resolver##{resolver.object_id}(#{resolver.state}) in pool##{@pool.object_id}" }
425
+ pin(resolver, selector)
435
426
 
436
427
  resolver
437
428
  end
@@ -446,14 +437,13 @@ module HTTPX
446
437
  conn1.log(level: 2) { "check-in connection##{conn1.object_id}(#{conn1.state}) in pool##{@pool.object_id}" }
447
438
  @pool.checkin_connection(conn1)
448
439
  end
449
- return false
440
+ return
450
441
  end
451
442
 
452
- conn2.log(level: 2) { "coalescing with conn##{conn1.object_id}[#{conn1.origin}])" }
453
- conn2.coalesce!(conn1)
443
+ conn2.log(level: 2) { "coalescing with connection##{conn1.object_id}[#{conn1.origin}])" }
454
444
  select_connection(conn1, selector) if from_pool
445
+ conn2.coalesce!(conn1)
455
446
  conn2.disconnect
456
- true
457
447
  end
458
448
 
459
449
  def get_current_selector
@@ -482,6 +472,7 @@ module HTTPX
482
472
  th.thread_variable_get(:httpx_persistent_selector_store)
483
473
  end
484
474
 
475
+ Options.freeze
485
476
  @default_options = Options.new
486
477
  @default_options.freeze
487
478
  @plugins = []
@@ -503,7 +494,6 @@ module HTTPX
503
494
  #
504
495
  def plugin(pl, options = nil, &block)
505
496
  label = pl
506
- # raise Error, "Cannot add a plugin to a frozen config" if frozen?
507
497
  pl = Plugins.load_plugin(pl) if pl.is_a?(Symbol)
508
498
  raise ArgumentError, "Invalid plugin type: #{pl.class.inspect}" unless pl.is_a?(Module)
509
499
 
@@ -12,7 +12,7 @@ module HTTPX::Transcoder
12
12
  def initialize(body)
13
13
  body = body.open(File::RDONLY, encoding: Encoding::BINARY) if Object.const_defined?(:Pathname) && body.is_a?(Pathname)
14
14
  @body = body
15
- super(body)
15
+ super
16
16
  end
17
17
 
18
18
  def bytesize
@@ -10,15 +10,20 @@ module HTTPX
10
10
  def deflate(chunk)
11
11
  @deflater ||= Zlib::Deflate.new
12
12
 
13
- if chunk.nil?
14
- unless @deflater.closed?
15
- last = @deflater.finish
16
- @deflater.close
17
- last.empty? ? nil : last
18
- end
19
- else
20
- @deflater.deflate(chunk)
13
+ unless chunk.nil?
14
+ chunk = @deflater.deflate(chunk)
15
+
16
+ # deflate call may return nil, while still
17
+ # retaining the last chunk in the deflater.
18
+ return chunk unless chunk.empty?
21
19
  end
20
+
21
+ return if @deflater.closed?
22
+
23
+ last = @deflater.finish
24
+ @deflater.close
25
+
26
+ last unless last.empty?
22
27
  end
23
28
  end
24
29
 
@@ -64,7 +64,7 @@ module HTTPX::Transcoder
64
64
  else
65
65
  require "json"
66
66
  def json_load(*args); ::JSON.parse(*args); end
67
- def json_dump(*args); ::JSON.dump(*args); end
67
+ def json_dump(*args); ::JSON.generate(*args); end
68
68
  end
69
69
  # rubocop:enable Style/SingleLineMethods
70
70
  end
@@ -60,7 +60,7 @@ module HTTPX
60
60
  when :idle
61
61
  raise Error, "payload does not start with boundary" unless @buffer.start_with?("#{@intermediate_boundary}#{CRLF}")
62
62
 
63
- @buffer = @buffer.byteslice(@intermediate_boundary.bytesize + 2..-1)
63
+ @buffer = @buffer.byteslice((@intermediate_boundary.bytesize + 2)..-1)
64
64
 
65
65
  @state = :part_header
66
66
  when :part_header
@@ -70,7 +70,7 @@ module HTTPX
70
70
  return unless idx
71
71
 
72
72
  # @type var head: String
73
- head = @buffer.byteslice(0..idx + 4 - 1)
73
+ head = @buffer.byteslice(0..(idx + 4 - 1))
74
74
 
75
75
  @buffer = @buffer.byteslice(head.bytesize..-1)
76
76
 
@@ -107,8 +107,8 @@ module HTTPX
107
107
  idx = @buffer.index(body_separator)
108
108
 
109
109
  if idx
110
- payload = @buffer.byteslice(0..idx - 1)
111
- @buffer = @buffer.byteslice(idx + body_separator.bytesize..-1)
110
+ payload = @buffer.byteslice(0..(idx - 1))
111
+ @buffer = @buffer.byteslice((idx + body_separator.bytesize)..-1)
112
112
  part << payload
113
113
  part.rewind if part.respond_to?(:rewind)
114
114
  @state = :parse_boundary
@@ -53,7 +53,7 @@ module HTTPX
53
53
 
54
54
  def to_parts(form)
55
55
  params = form.each_with_object([]) do |(key, val), aux|
56
- Transcoder.normalize_keys(key, val, MULTIPART_VALUE_COND) do |k, v|
56
+ Transcoder::Multipart.normalize_keys(key, val) do |k, v|
57
57
  next if v.nil?
58
58
 
59
59
  value, content_type, filename = Part.call(v)
@@ -7,20 +7,28 @@ require_relative "multipart/mime_type_detector"
7
7
 
8
8
  module HTTPX::Transcoder
9
9
  module Multipart
10
- MULTIPART_VALUE_COND = lambda do |value|
10
+ module_function
11
+
12
+ def multipart?(form_data)
13
+ form_data.any? do |_, v|
14
+ multipart_value?(v) ||
15
+ (v.respond_to?(:to_ary) && v.to_ary.any? { |av| multipart_value?(av) }) ||
16
+ (v.respond_to?(:to_hash) && v.to_hash.any? { |_, e| multipart_value?(e) })
17
+ end
18
+ end
19
+
20
+ def multipart_value?(value)
11
21
  value.respond_to?(:read) ||
12
22
  (value.respond_to?(:to_hash) &&
13
23
  value.key?(:body) &&
14
24
  (value.key?(:filename) || value.key?(:content_type)))
15
25
  end
16
26
 
17
- module_function
18
-
19
- def multipart?(form_data)
20
- form_data.any? do |_, v|
21
- Multipart::MULTIPART_VALUE_COND.call(v) ||
22
- (v.respond_to?(:to_ary) && v.to_ary.any?(&Multipart::MULTIPART_VALUE_COND)) ||
23
- (v.respond_to?(:to_hash) && v.to_hash.any? { |_, e| Multipart::MULTIPART_VALUE_COND.call(e) })
27
+ def normalize_keys(key, value, transcoder = self, &block)
28
+ if multipart_value?(value)
29
+ block.call(key.to_s, value)
30
+ else
31
+ HTTPX::Transcoder.normalize_keys(key, value, transcoder, &block)
24
32
  end
25
33
  end
26
34
 
@@ -28,8 +28,7 @@ module HTTPX
28
28
  begin
29
29
  chunk = @body.next
30
30
  if outbuf
31
- outbuf.clear.force_encoding(Encoding::BINARY)
32
- outbuf << chunk
31
+ outbuf.replace(chunk)
33
32
  else
34
33
  outbuf = chunk
35
34
  end
@@ -31,8 +31,7 @@ module HTTPX
31
31
  return unless compressed_chunk
32
32
 
33
33
  if outbuf
34
- outbuf.clear.force_encoding(Encoding::BINARY)
35
- outbuf << compressed_chunk
34
+ outbuf.replace(compressed_chunk)
36
35
  else
37
36
  compressed_chunk
38
37
  end