httpx 0.16.1 → 0.17.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.
- checksums.yaml +4 -4
- data/doc/release_notes/0_17_0.md +49 -0
- data/lib/httpx/adapters/webmock.rb +2 -2
- data/lib/httpx/chainable.rb +1 -1
- data/lib/httpx/connection/http1.rb +15 -9
- data/lib/httpx/connection/http2.rb +13 -10
- data/lib/httpx/connection.rb +4 -5
- data/lib/httpx/headers.rb +1 -1
- data/lib/httpx/options.rb +28 -6
- data/lib/httpx/parser/http1.rb +10 -6
- data/lib/httpx/plugins/digest_authentication.rb +4 -4
- data/lib/httpx/plugins/h2c.rb +7 -3
- data/lib/httpx/plugins/multipart/decoder.rb +187 -0
- data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
- data/lib/httpx/plugins/multipart/part.rb +2 -2
- data/lib/httpx/plugins/multipart.rb +14 -0
- data/lib/httpx/plugins/ntlm_authentication.rb +4 -4
- data/lib/httpx/plugins/proxy/ssh.rb +11 -4
- data/lib/httpx/plugins/proxy.rb +6 -4
- data/lib/httpx/plugins/stream.rb +2 -3
- data/lib/httpx/registry.rb +1 -1
- data/lib/httpx/request.rb +6 -7
- data/lib/httpx/resolver/resolver_mixin.rb +2 -1
- data/lib/httpx/response.rb +37 -30
- data/lib/httpx/selector.rb +4 -2
- data/lib/httpx/session.rb +15 -13
- data/lib/httpx/transcoder/form.rb +20 -0
- data/lib/httpx/transcoder/json.rb +12 -0
- data/lib/httpx/transcoder.rb +62 -1
- data/lib/httpx/utils.rb +2 -2
- data/lib/httpx/version.rb +1 -1
- data/sig/buffer.rbs +2 -2
- data/sig/chainable.rbs +6 -1
- data/sig/connection/http1.rbs +10 -4
- data/sig/connection/http2.rbs +16 -5
- data/sig/connection.rbs +4 -4
- data/sig/headers.rbs +19 -18
- data/sig/options.rbs +13 -5
- data/sig/parser/http1.rbs +3 -3
- data/sig/plugins/aws_sigv4.rbs +12 -3
- data/sig/plugins/basic_authentication.rbs +1 -1
- data/sig/plugins/multipart.rbs +64 -8
- data/sig/plugins/proxy.rbs +6 -6
- data/sig/request.rbs +11 -8
- data/sig/resolver/native.rbs +4 -2
- data/sig/resolver/resolver_mixin.rbs +1 -1
- data/sig/resolver/system.rbs +1 -1
- data/sig/response.rbs +8 -2
- data/sig/selector.rbs +8 -6
- data/sig/session.rbs +8 -14
- data/sig/transcoder/form.rbs +1 -0
- data/sig/transcoder/json.rbs +1 -0
- data/sig/transcoder.rbs +5 -4
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6b2befec2e4b0093acd45f7ef9ef448ad41516510710616aded46934f1e3981
|
4
|
+
data.tar.gz: a9858adfacbdc27e1097b98b958f7dd1614f1207c74ec4290a54268df6270f69
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 86276d59efaf3a15efe0a27fbd59bff2d005bb3bab83ee9917599854bf46eba1bfbb016b9df55172c41799d1fe82195e4bb7d82c008c1996814ec2e393be71b3
|
7
|
+
data.tar.gz: bd113e65cf5700f231992bb485f6c59511f114372d53078ea53c019a654e449e5012b2a1a19f889d79cee1eecd24345d3e03f3f9fa50aa6c69060138327e1d09
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# 0.17.0
|
2
|
+
|
3
|
+
## Features
|
4
|
+
|
5
|
+
### Response mime type decoders (#json, #form)
|
6
|
+
|
7
|
+
https://gitlab.com/honeyryderchuck/httpx/-/wikis/Response-Handling#response-decoding
|
8
|
+
|
9
|
+
Two new methods, `#json` and `#form`, were added to `HTTPX::Response`. As the name implies, they'll decode the raw payload into ruby objects you can work with.
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
# after HTTPX.get("https://api.smth/endpoint-returning-json")
|
13
|
+
response.json # same as JSON.dump(response.to_s)
|
14
|
+
```
|
15
|
+
|
16
|
+
Although not yet documented, integrating custom decoders is also possible (i.e. parsing HTML with `nokogiri` or something similar).
|
17
|
+
|
18
|
+
## Improvements
|
19
|
+
|
20
|
+
### Connection: reduce interest calculations
|
21
|
+
|
22
|
+
Due to it being an intensive task, internal interest calculation in connections was reduce to the bare minimum.
|
23
|
+
|
24
|
+
### Immutable Options, internal recycling of instances, improves memory usage in the happy path
|
25
|
+
|
26
|
+
A lot of effort went into avoiding generating options objects internally whenever necessary. This means, when sending several requests with the same set of options (the most common case in `httpx` usage), internally only one object is passed around. For that, the following improvements were done:
|
27
|
+
|
28
|
+
* `Options#merge` returns the same options the the options being merged are a subset of the current set of options (b126938a6547e09b726dd64298fb488891d938e9).
|
29
|
+
* `Session#build_request` bypasses instantiation of options if it receives an `Options` object (which happens internally in the happy path, if users don't call `#build_request` directly) (3d549817cb41d4b904102fdc61afe3ecd9170893).
|
30
|
+
* Improving internal `Session` APIs to not pass around options, and instead rely on accessing request options.
|
31
|
+
* `Options#to_hash` does not build internal garbage arrays anymore (cc02679b804f63798f5d2136a039be1624e96ab6).
|
32
|
+
|
33
|
+
### Reduce regexp operations in the HTTP/1 parser
|
34
|
+
|
35
|
+
Some code paths in the HTTP/1 parser still using regular expressions were replaced by string operations accomplishing the same.
|
36
|
+
|
37
|
+
### HTTP/1 improvements on the complexity of connection accounting calculations
|
38
|
+
|
39
|
+
Managing open HTTP/1 connections relies on operations calculating whether there are requests waiting for completion. This relied on traversing all requests for that connectionn (O(n)); it now only checks the completion state of the first and last request of that connection, given that all requests in HTTP/1 are sequential (O(1)); this optimization brings a big improvement to persistent and pipelined requests (65261217b1270913e4bb93717e8b8dcfa775565a).
|
40
|
+
|
41
|
+
## Bugfixes
|
42
|
+
|
43
|
+
* fixing HTTP/1 protocol uncompliant exposing multiple values for the "Host" header (e435dd0534314508262184fb03d83124d89d2079).
|
44
|
+
|
45
|
+
* Custom response finalizer introduced in 0.16.0 has been reverted. It was brought to my attention that `Tempfile` implementation already takes care of the file on GC (and `httpx` was duplicating), and the approach taken in `httpx` was buggy in several ways (not tolerant to forks, never recycled finalizers...) (aa3be21c890f92a41afcc7931f01dd24cc801f7c).
|
46
|
+
|
47
|
+
## Chore
|
48
|
+
|
49
|
+
RBS Typing improvements based on latest stdlib signatures additions, such as `openssl`, `digest`, `socket` and others.
|
@@ -19,7 +19,7 @@ module WebMock
|
|
19
19
|
module InstanceMethods
|
20
20
|
private
|
21
21
|
|
22
|
-
def send_requests(*requests
|
22
|
+
def send_requests(*requests)
|
23
23
|
request_signatures = requests.map do |request|
|
24
24
|
request_signature = _build_webmock_request_signature(request)
|
25
25
|
WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)
|
@@ -47,7 +47,7 @@ module WebMock
|
|
47
47
|
|
48
48
|
unless real_requests.empty?
|
49
49
|
reqs = real_requests.keys
|
50
|
-
reqs.zip(super(*reqs
|
50
|
+
reqs.zip(super(*reqs)).each do |req, res|
|
51
51
|
idx = real_requests[req]
|
52
52
|
|
53
53
|
if WebMock::CallbackRegistry.any_callbacks?
|
data/lib/httpx/chainable.rb
CHANGED
@@ -59,7 +59,12 @@ module HTTPX
|
|
59
59
|
def empty?
|
60
60
|
# this means that for every request there's an available
|
61
61
|
# partial response, so there are no in-flight requests waiting.
|
62
|
-
@requests.empty? ||
|
62
|
+
@requests.empty? || (
|
63
|
+
# checking all responses can be time-consuming. Alas, as in HTTP/1, responses
|
64
|
+
# do not come out of order, we can get away with checking first and last.
|
65
|
+
!@requests.first.response.nil? &&
|
66
|
+
(@requests.size == 1 || !@requests.last.response.nil?)
|
67
|
+
)
|
63
68
|
end
|
64
69
|
|
65
70
|
def <<(data)
|
@@ -260,7 +265,7 @@ module HTTPX
|
|
260
265
|
def set_protocol_headers(request)
|
261
266
|
if !request.headers.key?("content-length") &&
|
262
267
|
request.body.bytesize == Float::INFINITY
|
263
|
-
request.chunk!
|
268
|
+
request.body.chunk!
|
264
269
|
end
|
265
270
|
|
266
271
|
connection = request.headers["connection"]
|
@@ -285,10 +290,9 @@ module HTTPX
|
|
285
290
|
end
|
286
291
|
end
|
287
292
|
|
288
|
-
{
|
289
|
-
|
290
|
-
|
291
|
-
}
|
293
|
+
extra_headers = { "connection" => connection }
|
294
|
+
extra_headers["host"] = request.authority unless request.headers.key?("host")
|
295
|
+
extra_headers
|
292
296
|
end
|
293
297
|
|
294
298
|
def headline_uri(request)
|
@@ -318,7 +322,7 @@ module HTTPX
|
|
318
322
|
end
|
319
323
|
|
320
324
|
def join_body(request)
|
321
|
-
return if request.empty?
|
325
|
+
return if request.body.empty?
|
322
326
|
|
323
327
|
while (chunk = request.drain_body)
|
324
328
|
log(color: :green) { "<- DATA: #{chunk.bytesize} bytes..." }
|
@@ -327,7 +331,9 @@ module HTTPX
|
|
327
331
|
throw(:buffer_full, request) if @buffer.full?
|
328
332
|
end
|
329
333
|
|
330
|
-
|
334
|
+
return unless (error = request.drain_error)
|
335
|
+
|
336
|
+
raise error
|
331
337
|
end
|
332
338
|
|
333
339
|
def join_trailers(request)
|
@@ -354,7 +360,7 @@ module HTTPX
|
|
354
360
|
}.freeze
|
355
361
|
|
356
362
|
def capitalized(field)
|
357
|
-
UPCASED[field] || field.
|
363
|
+
UPCASED[field] || field.split("-").map(&:capitalize).join("-")
|
358
364
|
end
|
359
365
|
end
|
360
366
|
Connection.register "http/1.1", Connection::HTTP1
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "securerandom"
|
4
|
-
require "io/wait"
|
5
4
|
require "http/2/next"
|
6
5
|
|
7
6
|
module HTTPX
|
@@ -56,7 +55,7 @@ module HTTPX
|
|
56
55
|
|
57
56
|
return :w if !@pending.empty? && can_buffer_more_requests?
|
58
57
|
|
59
|
-
return :w
|
58
|
+
return :w unless @drains.empty?
|
60
59
|
|
61
60
|
if @buffer.empty?
|
62
61
|
return if @streams.empty? && @pings.empty?
|
@@ -218,7 +217,7 @@ module HTTPX
|
|
218
217
|
log(level: 1, color: :yellow) do
|
219
218
|
request.headers.merge(extra_headers).each.map { |k, v| "#{stream.id}: -> HEADER: #{k}: #{v}" }.join("\n")
|
220
219
|
end
|
221
|
-
stream.headers(request.headers.each(extra_headers), end_stream: request.empty?)
|
220
|
+
stream.headers(request.headers.each(extra_headers), end_stream: request.body.empty?)
|
222
221
|
end
|
223
222
|
|
224
223
|
def join_trailers(stream, request)
|
@@ -234,7 +233,7 @@ module HTTPX
|
|
234
233
|
end
|
235
234
|
|
236
235
|
def join_body(stream, request)
|
237
|
-
return if request.empty?
|
236
|
+
return if request.body.empty?
|
238
237
|
|
239
238
|
chunk = @drains.delete(request) || request.drain_body
|
240
239
|
while chunk
|
@@ -249,7 +248,9 @@ module HTTPX
|
|
249
248
|
chunk = next_chunk
|
250
249
|
end
|
251
250
|
|
252
|
-
|
251
|
+
return unless (error = request.drain_error)
|
252
|
+
|
253
|
+
on_stream_refuse(stream, request, error)
|
253
254
|
end
|
254
255
|
|
255
256
|
######
|
@@ -257,8 +258,10 @@ module HTTPX
|
|
257
258
|
######
|
258
259
|
|
259
260
|
def on_stream_headers(stream, request, h)
|
260
|
-
|
261
|
-
|
261
|
+
response = request.response
|
262
|
+
|
263
|
+
if response.is_a?(Response) && response.version == "2.0"
|
264
|
+
on_stream_trailers(stream, response, h)
|
262
265
|
return
|
263
266
|
end
|
264
267
|
|
@@ -274,11 +277,11 @@ module HTTPX
|
|
274
277
|
handle(request, stream) if request.expects?
|
275
278
|
end
|
276
279
|
|
277
|
-
def on_stream_trailers(stream,
|
280
|
+
def on_stream_trailers(stream, response, h)
|
278
281
|
log(color: :yellow) do
|
279
282
|
h.map { |k, v| "#{stream.id}: <- HEADER: #{k}: #{v}" }.join("\n")
|
280
283
|
end
|
281
|
-
|
284
|
+
response.merge_headers(h)
|
282
285
|
end
|
283
286
|
|
284
287
|
def on_stream_data(stream, request, data)
|
@@ -304,7 +307,7 @@ module HTTPX
|
|
304
307
|
emit(:response, request, response)
|
305
308
|
else
|
306
309
|
response = request.response
|
307
|
-
if response.status == 421
|
310
|
+
if response && response.status == 421
|
308
311
|
ex = MisdirectedRequestError.new(response)
|
309
312
|
ex.set_backtrace(caller)
|
310
313
|
emit(:error, request, ex)
|
data/lib/httpx/connection.rb
CHANGED
@@ -313,7 +313,7 @@ module HTTPX
|
|
313
313
|
|
314
314
|
# exit #consume altogether if all outstanding requests have been dealt with
|
315
315
|
return if @pending.size.zero? && @inflight.zero?
|
316
|
-
end unless (interests.nil? ||
|
316
|
+
end unless ((ints = interests).nil? || ints == :w || @state == :closing) && !epiped
|
317
317
|
|
318
318
|
#
|
319
319
|
# tight write loop.
|
@@ -360,19 +360,18 @@ module HTTPX
|
|
360
360
|
break if interests == :r || @state == :closing || @state == :closed
|
361
361
|
|
362
362
|
write_drained = false
|
363
|
-
end unless interests == :r
|
363
|
+
end unless (ints = interests) == :r
|
364
364
|
|
365
365
|
send_pending if @state == :open
|
366
366
|
|
367
367
|
# return if socket is drained
|
368
|
-
next unless (
|
369
|
-
(interests != :w || write_drained)
|
368
|
+
next unless (ints != :r || read_drained) && (ints != :w || write_drained)
|
370
369
|
|
371
370
|
# gotta go back to the event loop. It happens when:
|
372
371
|
#
|
373
372
|
# * the socket is drained of bytes or it's not the interest of the conn to read;
|
374
373
|
# * theres nothing more to write, or it's not in the interest of the conn to write;
|
375
|
-
log(level: 3) { "(#{
|
374
|
+
log(level: 3) { "(#{ints}): WAITING FOR EVENTS..." }
|
376
375
|
return
|
377
376
|
end
|
378
377
|
end
|
data/lib/httpx/headers.rb
CHANGED
data/lib/httpx/options.rb
CHANGED
@@ -39,6 +39,28 @@ module HTTPX
|
|
39
39
|
:resolver_options => { cache: true },
|
40
40
|
}.freeze
|
41
41
|
|
42
|
+
begin
|
43
|
+
module HashExtensions
|
44
|
+
refine Hash do
|
45
|
+
def >=(other)
|
46
|
+
Hash[other] <= self
|
47
|
+
end
|
48
|
+
|
49
|
+
def <=(other)
|
50
|
+
other = Hash[other]
|
51
|
+
return false unless size <= other.size
|
52
|
+
|
53
|
+
each do |k, v|
|
54
|
+
v2 = other.fetch(k) { return false }
|
55
|
+
return false unless v2 == v
|
56
|
+
end
|
57
|
+
true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
using HashExtensions
|
62
|
+
end unless Hash.method_defined?(:>=)
|
63
|
+
|
42
64
|
class << self
|
43
65
|
def new(options = {})
|
44
66
|
# let enhanced options go through
|
@@ -89,7 +111,7 @@ module HTTPX
|
|
89
111
|
|
90
112
|
def initialize(options = {})
|
91
113
|
defaults = DEFAULT_OPTIONS.merge(options)
|
92
|
-
defaults.each do |
|
114
|
+
defaults.each do |k, v|
|
93
115
|
next if v.nil?
|
94
116
|
|
95
117
|
begin
|
@@ -163,6 +185,7 @@ module HTTPX
|
|
163
185
|
end
|
164
186
|
|
165
187
|
REQUEST_IVARS = %i[@params @form @json @body].freeze
|
188
|
+
private_constant :REQUEST_IVARS
|
166
189
|
|
167
190
|
def ==(other)
|
168
191
|
ivars = instance_variables | other.instance_variables
|
@@ -180,14 +203,14 @@ module HTTPX
|
|
180
203
|
end
|
181
204
|
|
182
205
|
def merge(other)
|
183
|
-
raise ArgumentError, "#{other
|
206
|
+
raise ArgumentError, "#{other} is not a valid set of options" unless other.respond_to?(:to_hash)
|
184
207
|
|
185
208
|
h2 = other.to_hash
|
186
209
|
return self if h2.empty?
|
187
210
|
|
188
211
|
h1 = to_hash
|
189
212
|
|
190
|
-
return self if h1
|
213
|
+
return self if h1 >= h2
|
191
214
|
|
192
215
|
merged = h1.merge(h2) do |_k, v1, v2|
|
193
216
|
if v1.respond_to?(:merge) && v2.respond_to?(:merge)
|
@@ -201,10 +224,9 @@ module HTTPX
|
|
201
224
|
end
|
202
225
|
|
203
226
|
def to_hash
|
204
|
-
|
205
|
-
[ivar[1..-1].to_sym
|
227
|
+
instance_variables.each_with_object({}) do |ivar, hs|
|
228
|
+
hs[ivar[1..-1].to_sym] = instance_variable_get(ivar)
|
206
229
|
end
|
207
|
-
Hash[hash_pairs]
|
208
230
|
end
|
209
231
|
|
210
232
|
if RUBY_VERSION > "2.4.0"
|
data/lib/httpx/parser/http1.rb
CHANGED
@@ -60,7 +60,7 @@ module HTTPX
|
|
60
60
|
(m = %r{\AHTTP(?:/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?}in.match(@buffer)) ||
|
61
61
|
raise(Error, "wrong head line format")
|
62
62
|
version, code, _ = m.captures
|
63
|
-
raise(Error, "unsupported HTTP version (HTTP/#{version})") unless VERSIONS.include?(version)
|
63
|
+
raise(Error, "unsupported HTTP version (HTTP/#{version})") unless version && VERSIONS.include?(version)
|
64
64
|
|
65
65
|
@http_version = version.split(".").map(&:to_i)
|
66
66
|
@status_code = code.to_i
|
@@ -72,9 +72,14 @@ module HTTPX
|
|
72
72
|
|
73
73
|
def parse_headers
|
74
74
|
headers = @headers
|
75
|
-
|
76
|
-
|
77
|
-
|
75
|
+
buffer = @buffer
|
76
|
+
|
77
|
+
while (idx = buffer.index("\n"))
|
78
|
+
line = buffer.byteslice(0..idx)
|
79
|
+
raise Error, "wrong header format" if line.start_with?("\s", "\t")
|
80
|
+
|
81
|
+
line.lstrip!
|
82
|
+
buffer = @buffer = buffer.byteslice((idx + 1)..-1)
|
78
83
|
if line.empty?
|
79
84
|
case @state
|
80
85
|
when :headers
|
@@ -97,9 +102,8 @@ module HTTPX
|
|
97
102
|
raise Error, "wrong header format" unless separator_index
|
98
103
|
|
99
104
|
key = line.byteslice(0..(separator_index - 1))
|
100
|
-
raise Error, "wrong header format" if key.start_with?("\s", "\t")
|
101
105
|
|
102
|
-
key.
|
106
|
+
key.rstrip! # was lstripped previously!
|
103
107
|
value = line.byteslice((separator_index + 1)..-1)
|
104
108
|
value.strip!
|
105
109
|
raise Error, "wrong header format" if value.nil?
|
@@ -40,12 +40,12 @@ module HTTPX
|
|
40
40
|
|
41
41
|
alias_method :digest_auth, :digest_authentication
|
42
42
|
|
43
|
-
def send_requests(*requests
|
43
|
+
def send_requests(*requests)
|
44
44
|
requests.flat_map do |request|
|
45
45
|
digest = request.options.digest
|
46
46
|
|
47
47
|
if digest
|
48
|
-
probe_response = wrap { super(request
|
48
|
+
probe_response = wrap { super(request).first }
|
49
49
|
|
50
50
|
if digest && !probe_response.is_a?(ErrorResponse) &&
|
51
51
|
probe_response.status == 401 && probe_response.headers.key?("www-authenticate") &&
|
@@ -56,12 +56,12 @@ module HTTPX
|
|
56
56
|
token = digest.generate_header(request, probe_response)
|
57
57
|
request.headers["authorization"] = "Digest #{token}"
|
58
58
|
|
59
|
-
super(request
|
59
|
+
super(request)
|
60
60
|
else
|
61
61
|
probe_response
|
62
62
|
end
|
63
63
|
else
|
64
|
-
super(request
|
64
|
+
super(request)
|
65
65
|
end
|
66
66
|
end
|
67
67
|
end
|
data/lib/httpx/plugins/h2c.rb
CHANGED
@@ -24,15 +24,19 @@ module HTTPX
|
|
24
24
|
def call(connection, request, response)
|
25
25
|
connection.upgrade_to_h2c(request, response)
|
26
26
|
end
|
27
|
+
|
28
|
+
def extra_options(options)
|
29
|
+
options.merge(max_concurrent_requests: 1)
|
30
|
+
end
|
27
31
|
end
|
28
32
|
|
29
33
|
module InstanceMethods
|
30
|
-
def send_requests(*requests
|
34
|
+
def send_requests(*requests)
|
31
35
|
upgrade_request, *remainder = requests
|
32
36
|
|
33
37
|
return super unless VALID_H2C_VERBS.include?(upgrade_request.verb) && upgrade_request.scheme == "http"
|
34
38
|
|
35
|
-
connection = pool.find_connection(upgrade_request.uri,
|
39
|
+
connection = pool.find_connection(upgrade_request.uri, upgrade_request.options)
|
36
40
|
|
37
41
|
return super if connection && connection.upgrade_protocol == :h2c
|
38
42
|
|
@@ -42,7 +46,7 @@ module HTTPX
|
|
42
46
|
upgrade_request.headers["upgrade"] = "h2c"
|
43
47
|
upgrade_request.headers["http2-settings"] = HTTP2Next::Client.settings_header(upgrade_request.options.http2_settings)
|
44
48
|
|
45
|
-
super(upgrade_request, *remainder
|
49
|
+
super(upgrade_request, *remainder)
|
46
50
|
end
|
47
51
|
end
|
48
52
|
|