httpx 1.3.3 → 1.4.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/1_3_3.md +1 -1
  3. data/doc/release_notes/1_3_4.md +6 -0
  4. data/doc/release_notes/1_4_0.md +43 -0
  5. data/lib/httpx/adapters/faraday.rb +2 -0
  6. data/lib/httpx/adapters/webmock.rb +11 -5
  7. data/lib/httpx/callbacks.rb +0 -5
  8. data/lib/httpx/chainable.rb +3 -1
  9. data/lib/httpx/connection/http2.rb +11 -7
  10. data/lib/httpx/connection.rb +128 -16
  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/h2c.rb +23 -20
  21. data/lib/httpx/plugins/internal_telemetry.rb +27 -0
  22. data/lib/httpx/plugins/persistent.rb +16 -0
  23. data/lib/httpx/plugins/proxy/http.rb +17 -19
  24. data/lib/httpx/plugins/proxy.rb +91 -93
  25. data/lib/httpx/plugins/retries.rb +5 -8
  26. data/lib/httpx/plugins/upgrade.rb +5 -10
  27. data/lib/httpx/plugins/webdav.rb +6 -0
  28. data/lib/httpx/plugins/xml.rb +76 -0
  29. data/lib/httpx/pool.rb +73 -244
  30. data/lib/httpx/request/body.rb +16 -12
  31. data/lib/httpx/request.rb +1 -1
  32. data/lib/httpx/resolver/https.rb +12 -19
  33. data/lib/httpx/resolver/multi.rb +34 -16
  34. data/lib/httpx/resolver/native.rb +36 -13
  35. data/lib/httpx/resolver/resolver.rb +49 -11
  36. data/lib/httpx/resolver/system.rb +29 -11
  37. data/lib/httpx/resolver.rb +21 -14
  38. data/lib/httpx/response/body.rb +12 -1
  39. data/lib/httpx/response.rb +5 -3
  40. data/lib/httpx/selector.rb +164 -95
  41. data/lib/httpx/session.rb +296 -139
  42. data/lib/httpx/transcoder/gzip.rb +0 -3
  43. data/lib/httpx/transcoder/json.rb +14 -2
  44. data/lib/httpx/transcoder/multipart/encoder.rb +3 -1
  45. data/lib/httpx/transcoder/utils/deflater.rb +7 -4
  46. data/lib/httpx/transcoder/utils/inflater.rb +2 -0
  47. data/lib/httpx/transcoder.rb +0 -1
  48. data/lib/httpx/version.rb +1 -1
  49. data/lib/httpx.rb +19 -20
  50. data/sig/callbacks.rbs +0 -1
  51. data/sig/chainable.rbs +4 -0
  52. data/sig/connection/http2.rbs +1 -1
  53. data/sig/connection.rbs +14 -3
  54. data/sig/errors.rbs +6 -0
  55. data/sig/loggable.rbs +2 -0
  56. data/sig/options.rbs +7 -0
  57. data/sig/plugins/aws_sigv4.rbs +8 -2
  58. data/sig/plugins/content_digest.rbs +51 -0
  59. data/sig/plugins/cookies/cookie.rbs +9 -0
  60. data/sig/plugins/grpc/call.rbs +4 -0
  61. data/sig/plugins/persistent.rbs +4 -1
  62. data/sig/plugins/proxy/socks5.rbs +11 -3
  63. data/sig/plugins/proxy.rbs +18 -11
  64. data/sig/plugins/push_promise.rbs +3 -0
  65. data/sig/plugins/rate_limiter.rbs +2 -0
  66. data/sig/plugins/retries.rbs +1 -1
  67. data/sig/plugins/ssrf_filter.rbs +26 -0
  68. data/sig/plugins/webdav.rbs +23 -0
  69. data/sig/plugins/xml.rbs +37 -0
  70. data/sig/pool.rbs +25 -33
  71. data/sig/request/body.rbs +5 -1
  72. data/sig/resolver/multi.rbs +26 -1
  73. data/sig/resolver/native.rbs +0 -2
  74. data/sig/resolver/resolver.rbs +21 -2
  75. data/sig/resolver.rbs +5 -1
  76. data/sig/response/body.rbs +2 -2
  77. data/sig/response/buffer.rbs +2 -2
  78. data/sig/selector.rbs +30 -4
  79. data/sig/session.rbs +45 -18
  80. data/sig/transcoder/body.rbs +1 -1
  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/multipart.rbs +3 -3
  86. data/sig/transcoder/utils/body_reader.rbs +2 -2
  87. data/sig/transcoder/utils/deflater.rbs +2 -2
  88. metadata +12 -4
  89. data/lib/httpx/transcoder/xml.rb +0 -52
  90. 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,56 @@ 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
+ connection.current_session = self
73
+ connection.current_selector = selector
74
+ select_connection(connection, selector)
75
+ end
76
+ begin
77
+ @closing = true
78
+ selector.terminate
79
+ ensure
80
+ @closing = false
81
+ end
46
82
  end
47
83
 
48
84
  # performs one, or multple requests; it accepts:
@@ -89,113 +125,153 @@ module HTTPX
89
125
  request
90
126
  end
91
127
 
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
128
+ def select_connection(connection, selector)
129
+ selector.register(connection)
98
130
  end
99
131
 
100
- # callback executed when a response for a given request has been received.
101
- def on_response(request, response)
102
- @responses[request] = response
103
- end
132
+ alias_method :select_resolver, :select_connection
104
133
 
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
134
+ def deselect_connection(connection, selector, cloned = false)
135
+ selector.deregister(connection)
110
136
 
111
- # returns the corresponding HTTP::Response to the given +request+ if it has been received.
112
- def fetch_response(request, _, _)
113
- @responses.delete(request)
114
- end
137
+ # when connections coalesce
138
+ return if connection.state == :idle
115
139
 
116
- # returns the HTTPX::Connection through which the +request+ should be sent through.
117
- def find_connection(request, connections, options)
118
- uri = request.uri
140
+ return if cloned
119
141
 
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
142
+ return if @closing && connection.state == :closed
143
+
144
+ @pool.checkin_connection(connection)
126
145
  end
127
146
 
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)
147
+ def deselect_resolver(resolver, selector)
148
+ selector.deregister(resolver)
135
149
 
136
- request.emit(:response, ErrorResponse.new(request, error))
150
+ return if @closing && resolver.closed?
151
+
152
+ @pool.checkin_resolver(resolver)
137
153
  end
138
154
 
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)
155
+ def try_clone_connection(connection, selector, family)
156
+ connection.family ||= family
157
+
158
+ return connection if connection.family == family
159
+
160
+ new_connection = connection.class.new(connection.origin, connection.options)
161
+
162
+ new_connection.family = family
163
+ new_connection.current_session = self
164
+ new_connection.current_selector = selector
165
+
166
+ connection.once(:tcp_open) { new_connection.force_reset(true) }
167
+ connection.once(:connect_error) do |err|
168
+ if new_connection.connecting?
169
+ new_connection.merge(connection)
170
+ connection.emit(:cloned, new_connection)
171
+ connection.force_reset(true)
172
+ else
173
+ connection.__send__(:handle_error, err)
174
+ end
175
+ end
176
+
177
+ new_connection.once(:tcp_open) do |new_conn|
178
+ if new_conn != connection
179
+ new_conn.merge(connection)
180
+ connection.force_reset(true)
147
181
  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
182
  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
183
+ new_connection.once(:connect_error) do |err|
184
+ if connection.connecting?
185
+ # main connection has the requests
186
+ connection.merge(new_connection)
187
+ new_connection.emit(:cloned, connection)
188
+ new_connection.force_reset(true)
189
+ else
190
+ new_connection.__send__(:handle_error, err)
191
+ end
156
192
  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
193
+
194
+ do_init_connection(new_connection, selector)
195
+ new_connection
161
196
  end
162
197
 
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"
198
+ # returns the HTTPX::Connection through which the +request+ should be sent through.
199
+ def find_connection(request_uri, selector, options)
200
+ if (connection = selector.find_connection(request_uri, options))
201
+ return connection
202
+ end
167
203
 
168
- altsvc = AltSvc.cached_altsvc_set(origin, alt_params.merge("origin" => alt_origin))
204
+ connection = @pool.checkout_connection(request_uri, options)
205
+
206
+ connection.current_session = self
207
+ connection.current_selector = selector
208
+
209
+ case connection.state
210
+ when :idle
211
+ do_init_connection(connection, selector)
212
+ when :open
213
+ select_connection(connection, selector) if options.io
214
+ when :closed
215
+ connection.idling
216
+ select_connection(connection, selector)
217
+ when :closing
218
+ connection.once(:close) do
219
+ connection.idling
220
+ select_connection(connection, selector)
221
+ end
222
+ end
169
223
 
170
- # altsvc already exists, somehow it wasn't advertised, probably noop
171
- return unless altsvc
224
+ connection
225
+ end
172
226
 
173
- alt_options = options.merge(ssl: options.ssl.merge(hostname: URI(origin).host))
227
+ private
174
228
 
175
- connection = pool.find_connection(alt_origin, alt_options) || init_connection(alt_origin, alt_options)
229
+ def deactivate(selector)
230
+ selector.each_connection do |connection|
231
+ connection.deactivate
232
+ deselect_connection(connection, selector) if connection.state == :inactive
233
+ end
234
+ end
176
235
 
177
- # advertised altsvc is the same origin being used, ignore
178
- return if connection == existing_connection
236
+ # callback executed when a response for a given request has been received.
237
+ def on_response(request, response)
238
+ @responses[request] = response
239
+ end
179
240
 
180
- connection.extend(AltSvc::ConnectionMixin) unless connection.is_a?(AltSvc::ConnectionMixin)
241
+ # callback executed when an HTTP/2 promise frame has been received.
242
+ def on_promise(_, stream)
243
+ log(level: 2) { "#{stream.id}: refusing stream!" }
244
+ stream.refuse
245
+ end
181
246
 
182
- set_connection_callbacks(connection, connections, alt_options)
247
+ # returns the corresponding HTTP::Response to the given +request+ if it has been received.
248
+ def fetch_response(request, _selector, _options)
249
+ @responses.delete(request)
250
+ end
183
251
 
184
- log(level: 1) { "#{origin} alt-svc: #{alt_origin}" }
252
+ # sends the +request+ to the corresponding HTTPX::Connection
253
+ def send_request(request, selector, options = request.options)
254
+ error = begin
255
+ catch(:resolve_error) do
256
+ connection = find_connection(request.uri, selector, options)
257
+ connection.send(request)
258
+ end
259
+ rescue StandardError => e
260
+ e
261
+ end
262
+ return unless error && error.is_a?(Exception)
185
263
 
186
- connection.merge(existing_connection)
187
- existing_connection.terminate
188
- connection
189
- rescue UnsupportedSchemeError
190
- altsvc["noop"] = true
191
- nil
264
+ if error.is_a?(Error)
265
+ request.emit(:response, ErrorResponse.new(request, error))
266
+ else
267
+ raise error if selector.empty?
268
+ end
192
269
  end
193
270
 
194
271
  # returns a set of HTTPX::Request objects built from the given +args+ and +options+.
195
272
  def build_requests(*args, params)
196
273
  requests = if args.size == 1
197
274
  reqs = args.first
198
- # TODO: find a way to make requests share same options object
199
275
  reqs.map do |verb, uri, ps = EMPTY_HASH|
200
276
  request_params = params
201
277
  request_params = request_params.merge(ps) unless ps.empty?
@@ -204,7 +280,6 @@ module HTTPX
204
280
  else
205
281
  verb, uris = args
206
282
  if uris.respond_to?(:each)
207
- # TODO: find a way to make requests share same options object
208
283
  uris.enum_for(:each).map do |uri, ps = EMPTY_HASH|
209
284
  request_params = params
210
285
  request_params = request_params.merge(ps) unless ps.empty?
@@ -224,78 +299,160 @@ module HTTPX
224
299
  request.on(:promise, &method(:on_promise))
225
300
  end
226
301
 
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
302
+ def do_init_connection(connection, selector)
303
+ resolve_connection(connection, selector) unless connection.family
241
304
  end
242
305
 
243
306
  # sends an array of HTTPX::Request +requests+, returns the respective array of HTTPX::Response objects.
244
307
  def send_requests(*requests)
245
- connections = _send_requests(requests)
246
- receive_requests(requests, connections)
308
+ selector = get_current_selector { Selector.new }
309
+ begin
310
+ _send_requests(requests, selector)
311
+ receive_requests(requests, selector)
312
+ ensure
313
+ unless @wrapped
314
+ if @persistent
315
+ deactivate(selector)
316
+ else
317
+ close(selector)
318
+ end
319
+ end
320
+ end
247
321
  end
248
322
 
249
323
  # sends an array of HTTPX::Request objects
250
- def _send_requests(requests)
251
- connections = []
252
-
324
+ def _send_requests(requests, selector)
253
325
  requests.each do |request|
254
- send_request(request, connections)
326
+ send_request(request, selector)
255
327
  end
256
-
257
- connections
258
328
  end
259
329
 
260
330
  # returns the array of HTTPX::Response objects corresponding to the array of HTTPX::Request +requests+.
261
- def receive_requests(requests, connections)
331
+ def receive_requests(requests, selector)
262
332
  # @type var responses: Array[response]
263
333
  responses = []
264
334
 
265
- begin
266
- # guarantee ordered responses
267
- loop do
268
- request = requests.first
335
+ # guarantee ordered responses
336
+ loop do
337
+ request = requests.first
338
+
339
+ return responses unless request
269
340
 
270
- return responses unless request
341
+ catch(:coalesced) { selector.next_tick } until (response = fetch_response(request, selector, request.options))
342
+ request.emit(:complete, response)
271
343
 
272
- catch(:coalesced) { pool.next_tick } until (response = fetch_response(request, connections, request.options))
273
- request.emit(:complete, response)
344
+ responses << response
345
+ requests.shift
274
346
 
347
+ break if requests.empty?
348
+
349
+ next unless selector.empty?
350
+
351
+ # in some cases, the pool of connections might have been drained because there was some
352
+ # handshake error, and the error responses have already been emitted, but there was no
353
+ # opportunity to traverse the requests, hence we're returning only a fraction of the errors
354
+ # we were supposed to. This effectively fetches the existing responses and return them.
355
+ while (request = requests.shift)
356
+ response = fetch_response(request, selector, request.options)
357
+ request.emit(:complete, response) if response
275
358
  responses << response
276
- requests.shift
359
+ end
360
+ break
361
+ end
362
+ responses
363
+ end
277
364
 
278
- break if requests.empty?
365
+ def resolve_connection(connection, selector)
366
+ if connection.addresses || connection.open?
367
+ #
368
+ # there are two cases in which we want to activate initialization of
369
+ # connection immediately:
370
+ #
371
+ # 1. when the connection already has addresses, i.e. it doesn't need to
372
+ # resolve a name (not the same as name being an IP, yet)
373
+ # 2. when the connection is initialized with an external already open IO.
374
+ #
375
+ connection.once(:connect_error, &connection.method(:handle_error))
376
+ on_resolver_connection(connection, selector)
377
+ return
378
+ end
279
379
 
280
- next unless pool.empty?
380
+ resolver = find_resolver_for(connection, selector)
281
381
 
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
382
+ resolver.early_resolve(connection) || resolver.lazy_resolve(connection)
383
+ end
384
+
385
+ def on_resolver_connection(connection, selector)
386
+ from_pool = false
387
+ found_connection = selector.find_mergeable_connection(connection) || begin
388
+ from_pool = true
389
+ @pool.checkout_mergeable_connection(connection)
390
+ end
391
+
392
+ return select_connection(connection, selector) unless found_connection
393
+
394
+ if found_connection.open?
395
+ coalesce_connections(found_connection, connection, selector, from_pool)
396
+ else
397
+ found_connection.once(:open) do
398
+ coalesce_connections(found_connection, connection, selector, from_pool)
292
399
  end
293
- responses
294
- ensure
295
- if @persistent
296
- pool.deactivate(*connections)
297
- else
298
- close(connections)
400
+ end
401
+ end
402
+
403
+ def on_resolver_close(resolver, selector)
404
+ return if resolver.closed?
405
+
406
+ deselect_resolver(resolver, selector)
407
+ resolver.close unless resolver.closed?
408
+ end
409
+
410
+ def find_resolver_for(connection, selector)
411
+ resolver = selector.find_resolver(connection.options)
412
+
413
+ unless resolver
414
+ resolver = @pool.checkout_resolver(connection.options)
415
+ resolver.current_session = self
416
+ resolver.current_selector = selector
417
+ end
418
+
419
+ resolver
420
+ end
421
+
422
+ # coalesces +conn2+ into +conn1+. if +conn1+ was loaded from the connection pool
423
+ # (it is known via +from_pool+), then it adds its to the +selector+.
424
+ def coalesce_connections(conn1, conn2, selector, from_pool)
425
+ unless conn1.coalescable?(conn2)
426
+ select_connection(conn2, selector)
427
+ @pool.checkin_connection(conn1) if from_pool
428
+ return false
429
+ end
430
+
431
+ conn2.emit(:tcp_open, conn1)
432
+ conn1.merge(conn2)
433
+ conn2.coalesced_connection = conn1
434
+ select_connection(conn1, selector) if from_pool
435
+ deselect_connection(conn2, selector)
436
+ true
437
+ end
438
+
439
+ def get_current_selector
440
+ selector_store[self] || (yield if block_given?)
441
+ end
442
+
443
+ def set_current_selector(selector)
444
+ if selector
445
+ selector_store[self] = selector
446
+ else
447
+ selector_store.delete(self)
448
+ end
449
+ end
450
+
451
+ def selector_store
452
+ th_current = Thread.current
453
+ th_current.thread_variable_get(:httpx_persistent_selector_store) || begin
454
+ {}.compare_by_identity.tap do |store|
455
+ th_current.thread_variable_set(:httpx_persistent_selector_store, store)
299
456
  end
300
457
  end
301
458
  end
@@ -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
@@ -35,7 +35,9 @@ module HTTPX
35
35
 
36
36
  def rewind
37
37
  form = @form.each_with_object([]) do |(key, val), aux|
38
- val = val.reopen(val.path, File::RDONLY) if val.is_a?(File) && val.closed?
38
+ if val.respond_to?(:path) && val.respond_to?(:reopen) && val.respond_to?(:closed?) && val.closed?
39
+ val = val.reopen(val.path, File::RDONLY)
40
+ end
39
41
  val.rewind if val.respond_to?(:rewind)
40
42
  aux << [key, val]
41
43
  end
@@ -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"
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.3.3"
4
+ VERSION = "1.4.0"
5
5
  end