httpx 0.16.1 → 0.17.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|