httpx 1.3.4 → 1.4.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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_4_0.md +43 -0
  3. data/doc/release_notes/1_4_1.md +19 -0
  4. data/lib/httpx/adapters/datadog.rb +55 -83
  5. data/lib/httpx/adapters/faraday.rb +2 -0
  6. data/lib/httpx/adapters/webmock.rb +18 -6
  7. data/lib/httpx/callbacks.rb +0 -5
  8. data/lib/httpx/chainable.rb +3 -1
  9. data/lib/httpx/connection/http2.rb +12 -8
  10. data/lib/httpx/connection.rb +192 -22
  11. data/lib/httpx/errors.rb +12 -0
  12. data/lib/httpx/loggable.rb +5 -5
  13. data/lib/httpx/options.rb +26 -16
  14. data/lib/httpx/plugins/aws_sigv4.rb +31 -16
  15. data/lib/httpx/plugins/callbacks.rb +12 -2
  16. data/lib/httpx/plugins/circuit_breaker.rb +0 -5
  17. data/lib/httpx/plugins/content_digest.rb +202 -0
  18. data/lib/httpx/plugins/expect.rb +4 -3
  19. data/lib/httpx/plugins/follow_redirects.rb +7 -8
  20. data/lib/httpx/plugins/grpc/grpc_encoding.rb +2 -0
  21. data/lib/httpx/plugins/h2c.rb +23 -20
  22. data/lib/httpx/plugins/internal_telemetry.rb +27 -0
  23. data/lib/httpx/plugins/persistent.rb +16 -0
  24. data/lib/httpx/plugins/proxy/http.rb +17 -19
  25. data/lib/httpx/plugins/proxy.rb +91 -93
  26. data/lib/httpx/plugins/retries.rb +5 -8
  27. data/lib/httpx/plugins/upgrade.rb +5 -10
  28. data/lib/httpx/plugins/webdav.rb +6 -0
  29. data/lib/httpx/plugins/xml.rb +76 -0
  30. data/lib/httpx/pool.rb +73 -244
  31. data/lib/httpx/request/body.rb +25 -26
  32. data/lib/httpx/request.rb +7 -1
  33. data/lib/httpx/resolver/https.rb +15 -20
  34. data/lib/httpx/resolver/multi.rb +34 -16
  35. data/lib/httpx/resolver/native.rb +66 -25
  36. data/lib/httpx/resolver/resolver.rb +59 -15
  37. data/lib/httpx/resolver/system.rb +31 -15
  38. data/lib/httpx/resolver.rb +21 -14
  39. data/lib/httpx/response.rb +5 -3
  40. data/lib/httpx/selector.rb +160 -95
  41. data/lib/httpx/session.rb +273 -140
  42. data/lib/httpx/transcoder/body.rb +15 -31
  43. data/lib/httpx/transcoder/gzip.rb +0 -3
  44. data/lib/httpx/transcoder/json.rb +14 -2
  45. data/lib/httpx/transcoder/multipart/part.rb +1 -1
  46. data/lib/httpx/transcoder/utils/deflater.rb +7 -4
  47. data/lib/httpx/transcoder/utils/inflater.rb +2 -0
  48. data/lib/httpx/transcoder.rb +0 -1
  49. data/lib/httpx/version.rb +1 -1
  50. data/lib/httpx.rb +20 -21
  51. data/sig/callbacks.rbs +0 -1
  52. data/sig/chainable.rbs +4 -0
  53. data/sig/connection/http2.rbs +1 -1
  54. data/sig/connection.rbs +29 -3
  55. data/sig/errors.rbs +6 -0
  56. data/sig/loggable.rbs +2 -0
  57. data/sig/options.rbs +7 -0
  58. data/sig/plugins/aws_sigv4.rbs +8 -2
  59. data/sig/plugins/content_digest.rbs +51 -0
  60. data/sig/plugins/cookies/cookie.rbs +9 -0
  61. data/sig/plugins/grpc/call.rbs +4 -0
  62. data/sig/plugins/persistent.rbs +4 -1
  63. data/sig/plugins/proxy/socks5.rbs +11 -3
  64. data/sig/plugins/proxy.rbs +18 -11
  65. data/sig/plugins/push_promise.rbs +3 -0
  66. data/sig/plugins/rate_limiter.rbs +2 -0
  67. data/sig/plugins/retries.rbs +1 -1
  68. data/sig/plugins/ssrf_filter.rbs +26 -0
  69. data/sig/plugins/webdav.rbs +23 -0
  70. data/sig/plugins/xml.rbs +37 -0
  71. data/sig/pool.rbs +25 -33
  72. data/sig/request/body.rbs +5 -9
  73. data/sig/resolver/multi.rbs +26 -1
  74. data/sig/resolver/native.rbs +2 -2
  75. data/sig/resolver/resolver.rbs +21 -2
  76. data/sig/resolver.rbs +5 -1
  77. data/sig/response/buffer.rbs +1 -1
  78. data/sig/selector.rbs +30 -4
  79. data/sig/session.rbs +47 -18
  80. data/sig/transcoder/body.rbs +2 -4
  81. data/sig/transcoder/chunker.rbs +1 -1
  82. data/sig/transcoder/deflate.rbs +1 -0
  83. data/sig/transcoder/form.rbs +8 -0
  84. data/sig/transcoder/gzip.rbs +4 -1
  85. data/sig/transcoder/utils/body_reader.rbs +3 -3
  86. data/sig/transcoder/utils/deflater.rbs +3 -3
  87. metadata +12 -4
  88. data/lib/httpx/transcoder/xml.rb +0 -52
  89. data/sig/transcoder/xml.rbs +0 -22
data/lib/httpx/session.rb CHANGED
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTPX
4
- EMPTY_HASH = {}.freeze
5
-
6
4
  # Class implementing the APIs being used publicly.
7
5
  #
8
6
  # HTTPX.get(..) #=> delegating to an internal HTTPX::Session object.
@@ -19,6 +17,9 @@ module HTTPX
19
17
  @options = self.class.default_options.merge(options)
20
18
  @responses = {}
21
19
  @persistent = @options.persistent
20
+ @pool = @options.pool_class.new(@options.pool_options)
21
+ @wrapped = false
22
+ @closing = false
22
23
  wrap(&blk) if blk
23
24
  end
24
25
 
@@ -28,21 +29,54 @@ module HTTPX
28
29
  # http.get("https://wikipedia.com")
29
30
  # end # wikipedia connection closes here
30
31
  def wrap
31
- prev_persistent = @persistent
32
- @persistent = true
33
- pool.wrap do
34
- begin
35
- yield self
36
- ensure
37
- @persistent = prev_persistent
38
- close unless @persistent
32
+ prev_wrapped = @wrapped
33
+ @wrapped = true
34
+ was_initialized = false
35
+ current_selector = get_current_selector do
36
+ selector = Selector.new
37
+
38
+ set_current_selector(selector)
39
+
40
+ was_initialized = true
41
+
42
+ selector
43
+ end
44
+ begin
45
+ yield self
46
+ ensure
47
+ unless prev_wrapped
48
+ if @persistent
49
+ deactivate(current_selector)
50
+ else
51
+ close(current_selector)
52
+ end
39
53
  end
54
+ @wrapped = prev_wrapped
55
+ set_current_selector(nil) if was_initialized
40
56
  end
41
57
  end
42
58
 
43
- # closes all the active connections from the session
44
- def close(*args)
45
- pool.close(*args)
59
+ # closes all the active connections from the session.
60
+ #
61
+ # when called directly without specifying +selector+, all available connections
62
+ # will be picked up from the connection pool and closed. Connections in use
63
+ # by other sessions, or same session in a different thread, will not be reaped.
64
+ def close(selector = Selector.new)
65
+ # throw resolvers away from the pool
66
+ @pool.reset_resolvers
67
+
68
+ # preparing to throw away connections
69
+ while (connection = @pool.pop_connection)
70
+ next if connection.state == :closed
71
+
72
+ select_connection(connection, selector)
73
+ end
74
+ begin
75
+ @closing = true
76
+ selector.terminate
77
+ ensure
78
+ @closing = false
79
+ end
46
80
  end
47
81
 
48
82
  # performs one, or multple requests; it accepts:
@@ -89,113 +123,134 @@ module HTTPX
89
123
  request
90
124
  end
91
125
 
92
- private
93
-
94
- # returns the HTTPX::Pool object which manages the networking required to
95
- # perform requests.
96
- def pool
97
- Thread.current[:httpx_connection_pool] ||= Pool.new
126
+ def select_connection(connection, selector)
127
+ pin_connection(connection, selector)
128
+ selector.register(connection)
98
129
  end
99
130
 
100
- # callback executed when a response for a given request has been received.
101
- def on_response(request, response)
102
- @responses[request] = response
131
+ def pin_connection(connection, selector)
132
+ connection.current_session = self
133
+ connection.current_selector = selector
103
134
  end
104
135
 
105
- # callback executed when an HTTP/2 promise frame has been received.
106
- def on_promise(_, stream)
107
- log(level: 2) { "#{stream.id}: refusing stream!" }
108
- stream.refuse
109
- end
136
+ alias_method :select_resolver, :select_connection
110
137
 
111
- # returns the corresponding HTTP::Response to the given +request+ if it has been received.
112
- def fetch_response(request, _, _)
113
- @responses.delete(request)
138
+ def deselect_connection(connection, selector, cloned = false)
139
+ selector.deregister(connection)
140
+
141
+ # when connections coalesce
142
+ return if connection.state == :idle
143
+
144
+ return if cloned
145
+
146
+ return if @closing && connection.state == :closed
147
+
148
+ @pool.checkin_connection(connection)
114
149
  end
115
150
 
116
- # returns the HTTPX::Connection through which the +request+ should be sent through.
117
- def find_connection(request, connections, options)
118
- uri = request.uri
151
+ def deselect_resolver(resolver, selector)
152
+ selector.deregister(resolver)
119
153
 
120
- connection = pool.find_connection(uri, options) || init_connection(uri, options)
121
- unless connections.nil? || connections.include?(connection)
122
- connections << connection
123
- set_connection_callbacks(connection, connections, options)
124
- end
125
- connection
154
+ return if @closing && resolver.closed?
155
+
156
+ @pool.checkin_resolver(resolver)
126
157
  end
127
158
 
128
- # sends the +request+ to the corresponding HTTPX::Connection
129
- def send_request(request, connections, options = request.options)
130
- error = catch(:resolve_error) do
131
- connection = find_connection(request, connections, options)
132
- connection.send(request)
133
- end
134
- return unless error.is_a?(Error)
159
+ def try_clone_connection(connection, selector, family)
160
+ connection.family ||= family
161
+
162
+ return connection if connection.family == family
163
+
164
+ new_connection = connection.class.new(connection.origin, connection.options)
165
+
166
+ new_connection.family = family
167
+
168
+ connection.sibling = new_connection
135
169
 
136
- request.emit(:response, ErrorResponse.new(request, error))
170
+ do_init_connection(new_connection, selector)
171
+ new_connection
137
172
  end
138
173
 
139
- # sets the callbacks on the +connection+ required to process certain specific
140
- # connection lifecycle events which deal with request rerouting.
141
- def set_connection_callbacks(connection, connections, options, cloned: false)
142
- connection.only(:misdirected) do |misdirected_request|
143
- other_connection = connection.create_idle(ssl: { alpn_protocols: %w[http/1.1] })
144
- other_connection.merge(connection)
145
- catch(:coalesced) do
146
- pool.init_connection(other_connection, options)
147
- end
148
- set_connection_callbacks(other_connection, connections, options)
149
- connections << other_connection
150
- misdirected_request.transition(:idle)
151
- other_connection.send(misdirected_request)
152
- end
153
- connection.only(:altsvc) do |alt_origin, origin, alt_params|
154
- other_connection = build_altsvc_connection(connection, connections, alt_origin, origin, alt_params, options)
155
- connections << other_connection if other_connection
174
+ # returns the HTTPX::Connection through which the +request+ should be sent through.
175
+ def find_connection(request_uri, selector, options)
176
+ if (connection = selector.find_connection(request_uri, options))
177
+ return connection
156
178
  end
157
- connection.only(:cloned) do |cloned_conn|
158
- set_connection_callbacks(cloned_conn, connections, options, cloned: true)
159
- connections << cloned_conn
160
- end unless cloned
161
- end
162
179
 
163
- # returns an HTTPX::Connection for the negotiated Alternative Service (or none).
164
- def build_altsvc_connection(existing_connection, connections, alt_origin, origin, alt_params, options)
165
- # do not allow security downgrades on altsvc negotiation
166
- return if existing_connection.origin.scheme == "https" && alt_origin.scheme != "https"
180
+ connection = @pool.checkout_connection(request_uri, options)
167
181
 
168
- altsvc = AltSvc.cached_altsvc_set(origin, alt_params.merge("origin" => alt_origin))
182
+ case connection.state
183
+ when :idle
184
+ do_init_connection(connection, selector)
185
+ when :open
186
+ if options.io
187
+ select_connection(connection, selector)
188
+ else
189
+ pin_connection(connection, selector)
190
+ end
191
+ when :closed
192
+ connection.idling
193
+ select_connection(connection, selector)
194
+ when :closing
195
+ connection.once(:close) do
196
+ connection.idling
197
+ select_connection(connection, selector)
198
+ end
199
+ else
200
+ pin_connection(connection, selector)
201
+ end
169
202
 
170
- # altsvc already exists, somehow it wasn't advertised, probably noop
171
- return unless altsvc
203
+ connection
204
+ end
172
205
 
173
- alt_options = options.merge(ssl: options.ssl.merge(hostname: URI(origin).host))
206
+ private
174
207
 
175
- connection = pool.find_connection(alt_origin, alt_options) || init_connection(alt_origin, alt_options)
208
+ def deactivate(selector)
209
+ selector.each_connection do |connection|
210
+ connection.deactivate
211
+ deselect_connection(connection, selector) if connection.state == :inactive
212
+ end
213
+ end
176
214
 
177
- # advertised altsvc is the same origin being used, ignore
178
- return if connection == existing_connection
215
+ # callback executed when a response for a given request has been received.
216
+ def on_response(request, response)
217
+ @responses[request] = response
218
+ end
179
219
 
180
- connection.extend(AltSvc::ConnectionMixin) unless connection.is_a?(AltSvc::ConnectionMixin)
220
+ # callback executed when an HTTP/2 promise frame has been received.
221
+ def on_promise(_, stream)
222
+ log(level: 2) { "#{stream.id}: refusing stream!" }
223
+ stream.refuse
224
+ end
181
225
 
182
- set_connection_callbacks(connection, connections, alt_options)
226
+ # returns the corresponding HTTP::Response to the given +request+ if it has been received.
227
+ def fetch_response(request, _selector, _options)
228
+ @responses.delete(request)
229
+ end
183
230
 
184
- log(level: 1) { "#{origin} alt-svc: #{alt_origin}" }
231
+ # sends the +request+ to the corresponding HTTPX::Connection
232
+ def send_request(request, selector, options = request.options)
233
+ error = begin
234
+ catch(:resolve_error) do
235
+ connection = find_connection(request.uri, selector, options)
236
+ connection.send(request)
237
+ end
238
+ rescue StandardError => e
239
+ e
240
+ end
241
+ return unless error && error.is_a?(Exception)
185
242
 
186
- connection.merge(existing_connection)
187
- existing_connection.terminate
188
- connection
189
- rescue UnsupportedSchemeError
190
- altsvc["noop"] = true
191
- nil
243
+ if error.is_a?(Error)
244
+ request.emit(:response, ErrorResponse.new(request, error))
245
+ else
246
+ raise error if selector.empty?
247
+ end
192
248
  end
193
249
 
194
250
  # returns a set of HTTPX::Request objects built from the given +args+ and +options+.
195
251
  def build_requests(*args, params)
196
252
  requests = if args.size == 1
197
253
  reqs = args.first
198
- # TODO: find a way to make requests share same options object
199
254
  reqs.map do |verb, uri, ps = EMPTY_HASH|
200
255
  request_params = params
201
256
  request_params = request_params.merge(ps) unless ps.empty?
@@ -204,7 +259,6 @@ module HTTPX
204
259
  else
205
260
  verb, uris = args
206
261
  if uris.respond_to?(:each)
207
- # TODO: find a way to make requests share same options object
208
262
  uris.enum_for(:each).map do |uri, ps = EMPTY_HASH|
209
263
  request_params = params
210
264
  request_params = request_params.merge(ps) unless ps.empty?
@@ -224,78 +278,157 @@ module HTTPX
224
278
  request.on(:promise, &method(:on_promise))
225
279
  end
226
280
 
227
- def init_connection(uri, options)
228
- connection = options.connection_class.new(uri, options)
229
- catch(:coalesced) do
230
- pool.init_connection(connection, options)
231
- connection
232
- end
233
- end
234
-
235
- def deactivate_connection(request, connections, options)
236
- conn = connections.find do |c|
237
- c.match?(request.uri, options)
238
- end
239
-
240
- pool.deactivate(conn) if conn
281
+ def do_init_connection(connection, selector)
282
+ resolve_connection(connection, selector) unless connection.family
241
283
  end
242
284
 
243
285
  # sends an array of HTTPX::Request +requests+, returns the respective array of HTTPX::Response objects.
244
286
  def send_requests(*requests)
245
- connections = _send_requests(requests)
246
- receive_requests(requests, connections)
287
+ selector = get_current_selector { Selector.new }
288
+ begin
289
+ _send_requests(requests, selector)
290
+ receive_requests(requests, selector)
291
+ ensure
292
+ unless @wrapped
293
+ if @persistent
294
+ deactivate(selector)
295
+ else
296
+ close(selector)
297
+ end
298
+ end
299
+ end
247
300
  end
248
301
 
249
302
  # sends an array of HTTPX::Request objects
250
- def _send_requests(requests)
251
- connections = []
252
-
303
+ def _send_requests(requests, selector)
253
304
  requests.each do |request|
254
- send_request(request, connections)
305
+ send_request(request, selector)
255
306
  end
256
-
257
- connections
258
307
  end
259
308
 
260
309
  # returns the array of HTTPX::Response objects corresponding to the array of HTTPX::Request +requests+.
261
- def receive_requests(requests, connections)
310
+ def receive_requests(requests, selector)
262
311
  # @type var responses: Array[response]
263
312
  responses = []
264
313
 
265
- begin
266
- # guarantee ordered responses
267
- loop do
268
- request = requests.first
314
+ # guarantee ordered responses
315
+ loop do
316
+ request = requests.first
317
+
318
+ return responses unless request
269
319
 
270
- return responses unless request
320
+ catch(:coalesced) { selector.next_tick } until (response = fetch_response(request, selector, request.options))
321
+ request.emit(:complete, response)
271
322
 
272
- catch(:coalesced) { pool.next_tick } until (response = fetch_response(request, connections, request.options))
273
- request.emit(:complete, response)
323
+ responses << response
324
+ requests.shift
274
325
 
326
+ break if requests.empty?
327
+
328
+ next unless selector.empty?
329
+
330
+ # in some cases, the pool of connections might have been drained because there was some
331
+ # handshake error, and the error responses have already been emitted, but there was no
332
+ # opportunity to traverse the requests, hence we're returning only a fraction of the errors
333
+ # we were supposed to. This effectively fetches the existing responses and return them.
334
+ while (request = requests.shift)
335
+ response = fetch_response(request, selector, request.options)
336
+ request.emit(:complete, response) if response
275
337
  responses << response
276
- requests.shift
338
+ end
339
+ break
340
+ end
341
+ responses
342
+ end
277
343
 
278
- break if requests.empty?
344
+ def resolve_connection(connection, selector)
345
+ if connection.addresses || connection.open?
346
+ #
347
+ # there are two cases in which we want to activate initialization of
348
+ # connection immediately:
349
+ #
350
+ # 1. when the connection already has addresses, i.e. it doesn't need to
351
+ # resolve a name (not the same as name being an IP, yet)
352
+ # 2. when the connection is initialized with an external already open IO.
353
+ #
354
+ on_resolver_connection(connection, selector)
355
+ return
356
+ end
279
357
 
280
- next unless pool.empty?
358
+ resolver = find_resolver_for(connection, selector)
281
359
 
282
- # in some cases, the pool of connections might have been drained because there was some
283
- # handshake error, and the error responses have already been emitted, but there was no
284
- # opportunity to traverse the requests, hence we're returning only a fraction of the errors
285
- # we were supposed to. This effectively fetches the existing responses and return them.
286
- while (request = requests.shift)
287
- response = fetch_response(request, connections, request.options)
288
- request.emit(:complete, response) if response
289
- responses << response
290
- end
291
- break
360
+ resolver.early_resolve(connection) || resolver.lazy_resolve(connection)
361
+ end
362
+
363
+ def on_resolver_connection(connection, selector)
364
+ from_pool = false
365
+ found_connection = selector.find_mergeable_connection(connection) || begin
366
+ from_pool = true
367
+ @pool.checkout_mergeable_connection(connection)
368
+ end
369
+
370
+ return select_connection(connection, selector) unless found_connection
371
+
372
+ if found_connection.open?
373
+ coalesce_connections(found_connection, connection, selector, from_pool)
374
+ else
375
+ found_connection.once(:open) do
376
+ coalesce_connections(found_connection, connection, selector, from_pool)
292
377
  end
293
- responses
294
- ensure
295
- if @persistent
296
- pool.deactivate(*connections)
297
- else
298
- close(connections)
378
+ end
379
+ end
380
+
381
+ def on_resolver_close(resolver, selector)
382
+ return if resolver.closed?
383
+
384
+ deselect_resolver(resolver, selector)
385
+ resolver.close unless resolver.closed?
386
+ end
387
+
388
+ def find_resolver_for(connection, selector)
389
+ resolver = selector.find_resolver(connection.options)
390
+
391
+ unless resolver
392
+ resolver = @pool.checkout_resolver(connection.options)
393
+ resolver.current_session = self
394
+ resolver.current_selector = selector
395
+ end
396
+
397
+ resolver
398
+ end
399
+
400
+ # coalesces +conn2+ into +conn1+. if +conn1+ was loaded from the connection pool
401
+ # (it is known via +from_pool+), then it adds its to the +selector+.
402
+ def coalesce_connections(conn1, conn2, selector, from_pool)
403
+ unless conn1.coalescable?(conn2)
404
+ select_connection(conn2, selector)
405
+ @pool.checkin_connection(conn1) if from_pool
406
+ return false
407
+ end
408
+
409
+ conn2.coalesced_connection = conn1
410
+ select_connection(conn1, selector) if from_pool
411
+ deselect_connection(conn2, selector)
412
+ true
413
+ end
414
+
415
+ def get_current_selector
416
+ selector_store[self] || (yield if block_given?)
417
+ end
418
+
419
+ def set_current_selector(selector)
420
+ if selector
421
+ selector_store[self] = selector
422
+ else
423
+ selector_store.delete(self)
424
+ end
425
+ end
426
+
427
+ def selector_store
428
+ th_current = Thread.current
429
+ th_current.thread_variable_get(:httpx_persistent_selector_store) || begin
430
+ {}.compare_by_identity.tap do |store|
431
+ th_current.thread_variable_set(:httpx_persistent_selector_store, store)
299
432
  end
300
433
  end
301
434
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
3
+ require "delegate"
4
4
 
5
5
  module HTTPX::Transcoder
6
6
  module Body
@@ -8,48 +8,32 @@ module HTTPX::Transcoder
8
8
 
9
9
  module_function
10
10
 
11
- class Encoder
12
- extend Forwardable
13
-
14
- def_delegator :@raw, :to_s
15
-
16
- def_delegator :@raw, :==
17
-
11
+ class Encoder < SimpleDelegator
18
12
  def initialize(body)
19
- @raw = body
13
+ body = body.open(File::RDONLY, encoding: Encoding::BINARY) if Object.const_defined?(:Pathname) && body.is_a?(Pathname)
14
+ @body = body
15
+ super(body)
20
16
  end
21
17
 
22
18
  def bytesize
23
- if @raw.respond_to?(:bytesize)
24
- @raw.bytesize
25
- elsif @raw.respond_to?(:to_ary)
26
- @raw.sum(&:bytesize)
27
- elsif @raw.respond_to?(:size)
28
- @raw.size || Float::INFINITY
29
- elsif @raw.respond_to?(:length)
30
- @raw.length || Float::INFINITY
31
- elsif @raw.respond_to?(:each)
19
+ if @body.respond_to?(:bytesize)
20
+ @body.bytesize
21
+ elsif @body.respond_to?(:to_ary)
22
+ @body.sum(&:bytesize)
23
+ elsif @body.respond_to?(:size)
24
+ @body.size || Float::INFINITY
25
+ elsif @body.respond_to?(:length)
26
+ @body.length || Float::INFINITY
27
+ elsif @body.respond_to?(:each)
32
28
  Float::INFINITY
33
29
  else
34
- raise Error, "cannot determine size of body: #{@raw.inspect}"
30
+ raise Error, "cannot determine size of body: #{@body.inspect}"
35
31
  end
36
32
  end
37
33
 
38
34
  def content_type
39
35
  "application/octet-stream"
40
36
  end
41
-
42
- private
43
-
44
- def respond_to_missing?(meth, *args)
45
- @raw.respond_to?(meth, *args) || super
46
- end
47
-
48
- def method_missing(meth, *args, &block)
49
- return super unless @raw.respond_to?(meth)
50
-
51
- @raw.__send__(meth, *args, &block)
52
- end
53
37
  end
54
38
 
55
39
  def encode(body)
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
- require "uri"
5
- require "stringio"
6
3
  require "zlib"
7
4
 
8
5
  module HTTPX
@@ -6,7 +6,19 @@ module HTTPX::Transcoder
6
6
  module JSON
7
7
  module_function
8
8
 
9
- JSON_REGEX = %r{\bapplication/(?:vnd\.api\+|hal\+)?json\b}i.freeze
9
+ JSON_REGEX = %r{
10
+ \b
11
+ application/
12
+ # optional vendor specific type
13
+ (?:
14
+ # token as per https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
15
+ [!#$%&'*+\-.^_`|~0-9a-z]+
16
+ # literal plus sign
17
+ \+
18
+ )?
19
+ json
20
+ \b
21
+ }ix.freeze
10
22
 
11
23
  class Encoder
12
24
  extend Forwardable
@@ -45,7 +57,7 @@ module HTTPX::Transcoder
45
57
  def json_dump(*args); MultiJson.dump(*args); end
46
58
  elsif defined?(Oj)
47
59
  def json_load(response, *args); Oj.load(response.to_s, *args); end
48
- def json_dump(*args); Oj.dump(*args); end
60
+ def json_dump(obj, options = {}); Oj.dump(obj, { mode: :compat }.merge(options)); end
49
61
  elsif defined?(Yajl)
50
62
  def json_load(response, *args); Yajl::Parser.new(*args).parse(response.to_s); end
51
63
  def json_dump(*args); Yajl::Encoder.encode(*args); end
@@ -19,7 +19,7 @@ module HTTPX
19
19
  value = value[:body]
20
20
  end
21
21
 
22
- value = value.open(File::RDONLY) if Object.const_defined?(:Pathname) && value.is_a?(Pathname)
22
+ value = value.open(File::RDONLY, encoding: Encoding::BINARY) if Object.const_defined?(:Pathname) && value.is_a?(Pathname)
23
23
 
24
24
  if value.respond_to?(:path) && value.respond_to?(:read)
25
25
  # either a File, a Tempfile, or something else which has to quack like a file
@@ -1,13 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
3
  require_relative "body_reader"
5
4
 
6
5
  module HTTPX
7
6
  module Transcoder
8
7
  class Deflater
9
- extend Forwardable
10
-
11
8
  attr_reader :content_type
12
9
 
13
10
  def initialize(body)
@@ -51,6 +48,12 @@ module HTTPX
51
48
  @closed = true
52
49
  end
53
50
 
51
+ def rewind
52
+ return unless @buffer
53
+
54
+ @buffer.rewind
55
+ end
56
+
54
57
  private
55
58
 
56
59
  # rubocop:disable Naming/MemoizedInstanceVariableName
@@ -62,7 +65,7 @@ module HTTPX
62
65
  )
63
66
  ::IO.copy_stream(self, buffer)
64
67
 
65
- buffer.rewind
68
+ buffer.rewind if buffer.respond_to?(:rewind)
66
69
 
67
70
  @buffer = buffer
68
71
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module HTTPX
2
4
  module Transcoder
3
5
  class Inflater
@@ -86,7 +86,6 @@ end
86
86
  require "httpx/transcoder/body"
87
87
  require "httpx/transcoder/form"
88
88
  require "httpx/transcoder/json"
89
- require "httpx/transcoder/xml"
90
89
  require "httpx/transcoder/chunker"
91
90
  require "httpx/transcoder/deflate"
92
91
  require "httpx/transcoder/gzip"