httpx 0.10.1 → 0.11.3
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/README.md +11 -3
- data/doc/release_notes/0_10_1.md +1 -3
- data/doc/release_notes/0_10_2.md +5 -0
- data/doc/release_notes/0_11_0.md +76 -0
- data/doc/release_notes/0_11_1.md +1 -0
- data/doc/release_notes/0_11_2.md +5 -0
- data/doc/release_notes/0_11_3.md +5 -0
- data/lib/httpx/adapters/datadog.rb +205 -0
- data/lib/httpx/adapters/faraday.rb +0 -2
- data/lib/httpx/adapters/webmock.rb +123 -0
- data/lib/httpx/chainable.rb +1 -1
- data/lib/httpx/connection/http1.rb +10 -0
- data/lib/httpx/connection/http2.rb +4 -4
- data/lib/httpx/domain_name.rb +1 -3
- data/lib/httpx/errors.rb +2 -0
- data/lib/httpx/headers.rb +1 -0
- data/lib/httpx/io/ssl.rb +4 -8
- data/lib/httpx/io/udp.rb +4 -3
- data/lib/httpx/plugins/compression.rb +1 -1
- data/lib/httpx/plugins/cookies/set_cookie_parser.rb +1 -1
- data/lib/httpx/plugins/expect.rb +33 -8
- data/lib/httpx/plugins/multipart.rb +40 -35
- data/lib/httpx/plugins/multipart/encoder.rb +115 -0
- data/lib/httpx/plugins/multipart/mime_type_detector.rb +64 -0
- data/lib/httpx/plugins/multipart/part.rb +34 -0
- data/lib/httpx/plugins/proxy/socks5.rb +3 -2
- data/lib/httpx/plugins/push_promise.rb +2 -2
- data/lib/httpx/request.rb +21 -11
- data/lib/httpx/resolver.rb +7 -4
- data/lib/httpx/resolver/https.rb +4 -2
- data/lib/httpx/resolver/native.rb +10 -6
- data/lib/httpx/resolver/system.rb +1 -1
- data/lib/httpx/selector.rb +1 -0
- data/lib/httpx/session.rb +15 -18
- data/lib/httpx/transcoder.rb +6 -4
- data/lib/httpx/version.rb +1 -1
- data/sig/connection/http2.rbs +3 -4
- data/sig/headers.rbs +3 -0
- data/sig/plugins/multipart.rbs +27 -4
- data/sig/request.rbs +1 -1
- data/sig/resolver/https.rbs +2 -0
- data/sig/response.rbs +1 -1
- data/sig/session.rbs +1 -1
- data/sig/transcoder.rbs +2 -2
- data/sig/transcoder/body.rbs +2 -0
- data/sig/transcoder/form.rbs +7 -1
- data/sig/transcoder/json.rbs +3 -1
- metadata +40 -46
- data/sig/missing.rbs +0 -12
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins::Multipart
|
5
|
+
module MimeTypeDetector
|
6
|
+
module_function
|
7
|
+
|
8
|
+
DEFAULT_MIMETYPE = "application/octet-stream"
|
9
|
+
|
10
|
+
# inspired by https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/determine_mime_type.rb
|
11
|
+
if defined?(MIME::Types)
|
12
|
+
|
13
|
+
def call(_file, filename)
|
14
|
+
mime = MIME::Types.of(filename).first
|
15
|
+
mime.content_type if mime
|
16
|
+
end
|
17
|
+
|
18
|
+
elsif defined?(MimeMagic)
|
19
|
+
|
20
|
+
def call(file, *)
|
21
|
+
mime = MimeMagic.by_magic(file)
|
22
|
+
mime.type if mime
|
23
|
+
end
|
24
|
+
|
25
|
+
elsif system("which file", out: File::NULL)
|
26
|
+
require "open3"
|
27
|
+
|
28
|
+
def call(file, *)
|
29
|
+
return if file.eof? # file command returns "application/x-empty" for empty files
|
30
|
+
|
31
|
+
Open3.popen3(*%w[file --mime-type --brief -]) do |stdin, stdout, stderr, thread|
|
32
|
+
begin
|
33
|
+
::IO.copy_stream(file, stdin.binmode)
|
34
|
+
rescue Errno::EPIPE
|
35
|
+
end
|
36
|
+
file.rewind
|
37
|
+
stdin.close
|
38
|
+
|
39
|
+
status = thread.value
|
40
|
+
|
41
|
+
# call to file command failed
|
42
|
+
if status.nil? || !status.success?
|
43
|
+
$stderr.print(stderr.read)
|
44
|
+
else
|
45
|
+
|
46
|
+
output = stdout.read.strip
|
47
|
+
|
48
|
+
if output.include?("cannot open")
|
49
|
+
$stderr.print(output)
|
50
|
+
else
|
51
|
+
output
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
else
|
58
|
+
|
59
|
+
def call(*); end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins::Multipart
|
5
|
+
module Part
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def call(value)
|
9
|
+
# take out specialized objects of the way
|
10
|
+
if value.respond_to?(:filename) && value.respond_to?(:content_type) && value.respond_to?(:read)
|
11
|
+
return [value, value.content_type, value.filename]
|
12
|
+
end
|
13
|
+
|
14
|
+
content_type = filename = nil
|
15
|
+
|
16
|
+
if value.is_a?(Hash)
|
17
|
+
content_type = value[:content_type]
|
18
|
+
filename = value[:filename]
|
19
|
+
value = value[:body]
|
20
|
+
end
|
21
|
+
|
22
|
+
value = value.open(:binmode => true) if value.is_a?(Pathname)
|
23
|
+
|
24
|
+
if value.is_a?(File)
|
25
|
+
filename ||= File.basename(value.path)
|
26
|
+
content_type ||= MimeTypeDetector.call(value, filename) || "application/octet-stream"
|
27
|
+
[value, content_type, filename]
|
28
|
+
else
|
29
|
+
[StringIO.new(value.to_s), "text/plain"]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -159,9 +159,10 @@ module HTTPX
|
|
159
159
|
packet = [VERSION, CONNECT, 0].pack("C*")
|
160
160
|
begin
|
161
161
|
ip = IPAddr.new(uri.host)
|
162
|
-
raise Error, "Socks4 connection to #{ip} not supported" unless ip.ipv4?
|
163
162
|
|
164
|
-
|
163
|
+
ipcode = ip.ipv6? ? IPV6 : IPV4
|
164
|
+
|
165
|
+
packet << [ipcode].pack("C") << ip.hton
|
165
166
|
rescue IPAddr::InvalidAddressError
|
166
167
|
packet << [DOMAIN, uri.host.bytesize, uri.host].pack("CCA*")
|
167
168
|
end
|
@@ -70,8 +70,8 @@ module HTTPX
|
|
70
70
|
request.transition(:done)
|
71
71
|
response = request.response
|
72
72
|
response.mark_as_pushed!
|
73
|
-
stream.on(:data, &parser.method(:on_stream_data).curry[stream, request])
|
74
|
-
stream.on(:close, &parser.method(:on_stream_close).curry[stream, request])
|
73
|
+
stream.on(:data, &parser.method(:on_stream_data).curry(3)[stream, request])
|
74
|
+
stream.on(:close, &parser.method(:on_stream_close).curry(3)[stream, request])
|
75
75
|
end
|
76
76
|
end
|
77
77
|
end
|
data/lib/httpx/request.rb
CHANGED
@@ -81,6 +81,10 @@ module HTTPX
|
|
81
81
|
def response=(response)
|
82
82
|
return unless response
|
83
83
|
|
84
|
+
if response.status == 100
|
85
|
+
@informational_status = response.status
|
86
|
+
return
|
87
|
+
end
|
84
88
|
@response = response
|
85
89
|
end
|
86
90
|
|
@@ -170,6 +174,12 @@ module HTTPX
|
|
170
174
|
end
|
171
175
|
end
|
172
176
|
|
177
|
+
def rewind
|
178
|
+
return if empty?
|
179
|
+
|
180
|
+
@body.rewind if @body.respond_to?(:rewind)
|
181
|
+
end
|
182
|
+
|
173
183
|
def empty?
|
174
184
|
return true if @body.nil?
|
175
185
|
return false if chunked?
|
@@ -212,6 +222,7 @@ module HTTPX
|
|
212
222
|
def transition(nextstate)
|
213
223
|
case nextstate
|
214
224
|
when :idle
|
225
|
+
@body.rewind
|
215
226
|
@response = nil
|
216
227
|
@drainer = nil
|
217
228
|
when :headers
|
@@ -221,15 +232,15 @@ module HTTPX
|
|
221
232
|
@state == :expect
|
222
233
|
|
223
234
|
if @headers.key?("expect")
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
235
|
+
if @informational_status && @informational_status == 100
|
236
|
+
# check for 100 Continue response, and deallocate the var
|
237
|
+
# if @informational_status == 100
|
238
|
+
# @response = nil
|
239
|
+
# end
|
240
|
+
else
|
241
|
+
return if @state == :expect # do not re-set it
|
242
|
+
|
243
|
+
nextstate = :expect
|
233
244
|
end
|
234
245
|
end
|
235
246
|
when :done
|
@@ -241,8 +252,7 @@ module HTTPX
|
|
241
252
|
end
|
242
253
|
|
243
254
|
def expects?
|
244
|
-
@headers["expect"] == "100-continue" &&
|
245
|
-
@response && @response.status == 100
|
255
|
+
@headers["expect"] == "100-continue" && @informational_status == 100 && !@response
|
246
256
|
end
|
247
257
|
|
248
258
|
class ProcIO
|
data/lib/httpx/resolver.rb
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "resolv"
|
4
|
-
require "httpx/resolver/resolver_mixin"
|
5
|
-
require "httpx/resolver/system"
|
6
|
-
require "httpx/resolver/native"
|
7
|
-
require "httpx/resolver/https"
|
8
4
|
|
9
5
|
module HTTPX
|
10
6
|
module Resolver
|
11
7
|
extend Registry
|
12
8
|
|
9
|
+
RESOLVE_TIMEOUT = 5
|
10
|
+
|
11
|
+
require "httpx/resolver/resolver_mixin"
|
12
|
+
require "httpx/resolver/system"
|
13
|
+
require "httpx/resolver/native"
|
14
|
+
require "httpx/resolver/https"
|
15
|
+
|
13
16
|
register :system, System
|
14
17
|
register :native, Native
|
15
18
|
register :https, HTTPS
|
data/lib/httpx/resolver/https.rb
CHANGED
@@ -37,12 +37,14 @@ module HTTPX
|
|
37
37
|
@connections = []
|
38
38
|
@uri = URI(@resolver_options[:uri])
|
39
39
|
@uri_addresses = nil
|
40
|
+
@resolver = Resolv::DNS.new
|
41
|
+
@resolver.timeouts = @resolver_options.fetch(:timeouts, Resolver::RESOLVE_TIMEOUT)
|
40
42
|
end
|
41
43
|
|
42
44
|
def <<(connection)
|
43
45
|
return if @uri.origin == connection.origin.to_s
|
44
46
|
|
45
|
-
@uri_addresses ||=
|
47
|
+
@uri_addresses ||= ip_resolve(@uri.host) || system_resolve(@uri.host) || @resolver.getaddresses(@uri.host)
|
46
48
|
|
47
49
|
if @uri_addresses.empty?
|
48
50
|
ex = ResolveError.new("Can't resolve DNS server #{@uri.host}")
|
@@ -99,7 +101,7 @@ module HTTPX
|
|
99
101
|
log { "resolver: query #{type} for #{hostname}" }
|
100
102
|
begin
|
101
103
|
request = build_request(hostname, type)
|
102
|
-
request.on(:response, &method(:on_response).curry[request])
|
104
|
+
request.on(:response, &method(:on_response).curry(2)[request])
|
103
105
|
request.on(:promise, &method(:on_promise))
|
104
106
|
@requests[request] = connection
|
105
107
|
resolver_connection.send(request)
|
@@ -9,7 +9,6 @@ module HTTPX
|
|
9
9
|
include Resolver::ResolverMixin
|
10
10
|
using URIExtensions
|
11
11
|
|
12
|
-
RESOLVE_TIMEOUT = 5
|
13
12
|
RECORD_TYPES = {
|
14
13
|
"A" => Resolv::DNS::Resource::IN::A,
|
15
14
|
"AAAA" => Resolv::DNS::Resource::IN::AAAA,
|
@@ -19,7 +18,7 @@ module HTTPX
|
|
19
18
|
{
|
20
19
|
**Resolv::DNS::Config.default_config_hash,
|
21
20
|
packet_size: 512,
|
22
|
-
timeouts: RESOLVE_TIMEOUT,
|
21
|
+
timeouts: Resolver::RESOLVE_TIMEOUT,
|
23
22
|
record_types: RECORD_TYPES.keys,
|
24
23
|
}.freeze
|
25
24
|
else
|
@@ -27,7 +26,7 @@ module HTTPX
|
|
27
26
|
nameserver: nil,
|
28
27
|
**Resolv::DNS::Config.default_config_hash,
|
29
28
|
packet_size: 512,
|
30
|
-
timeouts: RESOLVE_TIMEOUT,
|
29
|
+
timeouts: Resolver::RESOLVE_TIMEOUT,
|
31
30
|
record_types: RECORD_TYPES.keys,
|
32
31
|
}.freeze
|
33
32
|
end
|
@@ -148,14 +147,19 @@ module HTTPX
|
|
148
147
|
queries[h] = connection
|
149
148
|
next
|
150
149
|
end
|
150
|
+
|
151
151
|
@timeouts[host].shift
|
152
152
|
if @timeouts[host].empty?
|
153
153
|
@timeouts.delete(host)
|
154
154
|
@connections.delete(connection)
|
155
|
-
|
155
|
+
# This loop_time passed to the exception is bogus. Ideally we would pass the total
|
156
|
+
# resolve timeout, including from the previous retries.
|
157
|
+
raise ResolveTimeoutError.new(loop_time, "Timed out")
|
158
|
+
# raise NativeResolveError.new(connection, host)
|
156
159
|
else
|
160
|
+
log { "resolver: timeout after #{timeout}s, retry(#{@timeouts[host].first}) #{host}..." }
|
157
161
|
connections << connection
|
158
|
-
|
162
|
+
queries[h] = connection
|
159
163
|
end
|
160
164
|
end
|
161
165
|
@queries = queries
|
@@ -279,7 +283,7 @@ module HTTPX
|
|
279
283
|
@io.connect
|
280
284
|
return unless @io.connected?
|
281
285
|
|
282
|
-
resolve if @queries.empty?
|
286
|
+
resolve if @queries.empty? && !@connections.empty?
|
283
287
|
when :closed
|
284
288
|
return unless @state == :open
|
285
289
|
|
@@ -20,7 +20,7 @@ module HTTPX
|
|
20
20
|
timeouts = resolv_options.delete(:timeouts)
|
21
21
|
resolv_options.delete(:cache)
|
22
22
|
@resolver = Resolv::DNS.new(resolv_options.empty? ? nil : resolv_options)
|
23
|
-
@resolver.timeouts = timeouts
|
23
|
+
@resolver.timeouts = timeouts || Resolver::RESOLVE_TIMEOUT
|
24
24
|
end
|
25
25
|
|
26
26
|
def closed?
|
data/lib/httpx/selector.rb
CHANGED
data/lib/httpx/session.rb
CHANGED
@@ -41,7 +41,7 @@ module HTTPX
|
|
41
41
|
def build_request(verb, uri, options = EMPTY_HASH)
|
42
42
|
rklass = @options.request_class
|
43
43
|
request = rklass.new(verb, uri, @options.merge(options).merge(persistent: @persistent))
|
44
|
-
request.on(:response, &method(:on_response).curry[request])
|
44
|
+
request.on(:response, &method(:on_response).curry(2)[request])
|
45
45
|
request.on(:promise, &method(:on_promise))
|
46
46
|
request
|
47
47
|
end
|
@@ -136,23 +136,20 @@ module HTTPX
|
|
136
136
|
def build_requests(*args, options)
|
137
137
|
request_options = @options.merge(options)
|
138
138
|
|
139
|
-
requests =
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
end
|
154
|
-
else
|
155
|
-
raise ArgumentError, "unsupported number of arguments"
|
139
|
+
requests = if args.size == 1
|
140
|
+
reqs = args.first
|
141
|
+
reqs.map do |verb, uri, opts = EMPTY_HASH|
|
142
|
+
build_request(verb, uri, request_options.merge(opts))
|
143
|
+
end
|
144
|
+
else
|
145
|
+
verb, uris = args
|
146
|
+
if uris.respond_to?(:each)
|
147
|
+
uris.enum_for(:each).map do |uri, opts = EMPTY_HASH|
|
148
|
+
build_request(verb, uri, request_options.merge(opts))
|
149
|
+
end
|
150
|
+
else
|
151
|
+
[build_request(verb, uris, request_options)]
|
152
|
+
end
|
156
153
|
end
|
157
154
|
raise ArgumentError, "wrong number of URIs (given 0, expect 1..+1)" if requests.empty?
|
158
155
|
|
data/lib/httpx/transcoder.rb
CHANGED
@@ -4,18 +4,20 @@ module HTTPX
|
|
4
4
|
module Transcoder
|
5
5
|
extend Registry
|
6
6
|
|
7
|
-
def self.normalize_keys(key, value, &block)
|
8
|
-
if
|
7
|
+
def self.normalize_keys(key, value, cond = nil, &block)
|
8
|
+
if (cond && cond.call(value))
|
9
|
+
block.call(key.to_s, value)
|
10
|
+
elsif value.respond_to?(:to_ary)
|
9
11
|
if value.empty?
|
10
12
|
block.call("#{key}[]")
|
11
13
|
else
|
12
14
|
value.to_ary.each do |element|
|
13
|
-
normalize_keys("#{key}[]", element, &block)
|
15
|
+
normalize_keys("#{key}[]", element, cond, &block)
|
14
16
|
end
|
15
17
|
end
|
16
18
|
elsif value.respond_to?(:to_hash)
|
17
19
|
value.to_hash.each do |child_key, child_value|
|
18
|
-
normalize_keys("#{key}[#{child_key}]", child_value, &block)
|
20
|
+
normalize_keys("#{key}[#{child_key}]", child_value, cond, &block)
|
19
21
|
end
|
20
22
|
else
|
21
23
|
block.call(key.to_s, value)
|
data/lib/httpx/version.rb
CHANGED
data/sig/connection/http2.rbs
CHANGED
@@ -53,12 +53,11 @@ module HTTPX
|
|
53
53
|
|
54
54
|
def join_body: (HTTP2Next::Stream stream, Request request) -> void
|
55
55
|
|
56
|
+
def on_stream_headers: (HTTP2Next::Stream stream, Request request, Array[[String, String]] headers) -> void
|
56
57
|
|
57
|
-
|
58
|
+
def on_stream_data: (HTTP2Next::Stream stream, Request request, string data) -> void
|
58
59
|
|
59
|
-
|
60
|
-
|
61
|
-
# def on_stream_close: (HTTP2Next::Stream stream, Request request, Symbol? error) -> void
|
60
|
+
def on_stream_close: (HTTP2Next::Stream stream, Request request, Symbol? error) -> void
|
62
61
|
|
63
62
|
def on_frame: (string bytes) -> void
|
64
63
|
|
data/sig/headers.rbs
CHANGED
data/sig/plugins/multipart.rbs
CHANGED
@@ -3,18 +3,41 @@ module HTTPX
|
|
3
3
|
module Multipart
|
4
4
|
def self.load_dependencies: (singleton(Session)) -> void
|
5
5
|
def self.configure: (*untyped) -> void
|
6
|
-
def self?.encode: (untyped) -> Encoder
|
6
|
+
def self?.encode: (untyped) -> (Encoder | Transcoder::Form::Encoder)
|
7
|
+
|
8
|
+
type multipart_value = string | Pathname | File | _Reader
|
9
|
+
|
10
|
+
type record_multipart_value = multipart_value |
|
11
|
+
{ content_type: String, filename: String, body: multipart_value } |
|
12
|
+
{ content_type: String, body: multipart_value }
|
13
|
+
|
14
|
+
type multipart_nested_value = multipart_value | _ToAry[multipart_value] | _ToHash[string, multipart_value]
|
15
|
+
|
16
|
+
type multipart_input = Enumerable[[string, multipart_value], untyped]
|
7
17
|
|
8
18
|
class Encoder
|
9
19
|
include Transcoder::_Encoder
|
10
|
-
include _ToS
|
11
20
|
include _Reader
|
12
21
|
|
22
|
+
def content_type: () -> String
|
23
|
+
|
13
24
|
private
|
14
25
|
|
15
|
-
def initialize: (Hash[Symbol | string,
|
26
|
+
def initialize: (Hash[Symbol | string, multipart_nested_value] multipart_data) -> untyped
|
27
|
+
|
28
|
+
def header_part: (string key, String content_type, String? filename) -> StringIO
|
29
|
+
|
30
|
+
def read_chunks: (String buffer, Integer? length) -> void
|
31
|
+
|
32
|
+
def read_from_part: (Integer? max_length) -> void
|
33
|
+
end
|
34
|
+
|
35
|
+
module Part
|
36
|
+
def self?.call: (multipart_nested_value) -> ([_Reader, String, String?] | [_Reader, String])
|
37
|
+
end
|
16
38
|
|
17
|
-
|
39
|
+
module MimeTypeDetector
|
40
|
+
def self?.call: (IO file, ?String filename) -> String?
|
18
41
|
end
|
19
42
|
end
|
20
43
|
end
|