httpx 0.12.0 → 0.14.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.
- checksums.yaml +4 -4
- data/doc/release_notes/0_10_1.md +1 -1
- data/doc/release_notes/0_13_0.md +58 -0
- data/doc/release_notes/0_13_1.md +5 -0
- data/doc/release_notes/0_13_2.md +9 -0
- data/doc/release_notes/0_14_0.md +79 -0
- data/doc/release_notes/0_14_1.md +7 -0
- data/lib/httpx.rb +1 -2
- data/lib/httpx/callbacks.rb +12 -3
- data/lib/httpx/chainable.rb +2 -2
- data/lib/httpx/connection.rb +29 -22
- data/lib/httpx/connection/http1.rb +35 -15
- data/lib/httpx/connection/http2.rb +61 -15
- data/lib/httpx/headers.rb +7 -3
- data/lib/httpx/io/ssl.rb +30 -17
- data/lib/httpx/io/tcp.rb +48 -27
- data/lib/httpx/io/udp.rb +31 -7
- data/lib/httpx/io/unix.rb +27 -12
- data/lib/httpx/options.rb +97 -74
- data/lib/httpx/plugins/aws_sdk_authentication.rb +5 -2
- data/lib/httpx/plugins/aws_sigv4.rb +5 -4
- data/lib/httpx/plugins/basic_authentication.rb +8 -3
- data/lib/httpx/plugins/compression.rb +24 -12
- data/lib/httpx/plugins/compression/brotli.rb +10 -7
- data/lib/httpx/plugins/compression/deflate.rb +6 -5
- data/lib/httpx/plugins/compression/gzip.rb +4 -3
- data/lib/httpx/plugins/cookies.rb +3 -7
- data/lib/httpx/plugins/digest_authentication.rb +5 -5
- data/lib/httpx/plugins/expect.rb +6 -6
- data/lib/httpx/plugins/follow_redirects.rb +4 -4
- data/lib/httpx/plugins/grpc.rb +247 -0
- data/lib/httpx/plugins/grpc/call.rb +62 -0
- data/lib/httpx/plugins/grpc/message.rb +85 -0
- data/lib/httpx/plugins/h2c.rb +43 -58
- data/lib/httpx/plugins/internal_telemetry.rb +1 -1
- data/lib/httpx/plugins/multipart/part.rb +2 -2
- data/lib/httpx/plugins/proxy.rb +3 -7
- data/lib/httpx/plugins/proxy/http.rb +5 -4
- data/lib/httpx/plugins/proxy/ssh.rb +3 -3
- data/lib/httpx/plugins/rate_limiter.rb +1 -1
- data/lib/httpx/plugins/retries.rb +14 -15
- data/lib/httpx/plugins/stream.rb +99 -75
- data/lib/httpx/plugins/upgrade.rb +84 -0
- data/lib/httpx/plugins/upgrade/h2.rb +54 -0
- data/lib/httpx/pool.rb +14 -5
- data/lib/httpx/request.rb +25 -2
- data/lib/httpx/resolver/native.rb +7 -3
- data/lib/httpx/response.rb +9 -5
- data/lib/httpx/session.rb +17 -7
- data/lib/httpx/transcoder/chunker.rb +1 -1
- data/lib/httpx/version.rb +1 -1
- data/sig/callbacks.rbs +2 -0
- data/sig/chainable.rbs +2 -1
- data/sig/connection/http1.rbs +6 -1
- data/sig/connection/http2.rbs +6 -2
- data/sig/headers.rbs +2 -2
- data/sig/options.rbs +16 -22
- data/sig/plugins/aws_sdk_authentication.rbs +2 -0
- data/sig/plugins/aws_sigv4.rbs +0 -1
- data/sig/plugins/basic_authentication.rbs +2 -0
- data/sig/plugins/compression.rbs +7 -5
- data/sig/plugins/compression/brotli.rbs +1 -1
- data/sig/plugins/compression/deflate.rbs +1 -1
- data/sig/plugins/compression/gzip.rbs +1 -1
- data/sig/plugins/cookies.rbs +0 -1
- data/sig/plugins/digest_authentication.rbs +0 -1
- data/sig/plugins/expect.rbs +0 -2
- data/sig/plugins/follow_redirects.rbs +0 -2
- data/sig/plugins/h2c.rbs +5 -10
- data/sig/plugins/persistent.rbs +0 -1
- data/sig/plugins/proxy.rbs +0 -1
- data/sig/plugins/retries.rbs +0 -4
- data/sig/plugins/stream.rbs +17 -16
- data/sig/plugins/upgrade.rbs +23 -0
- data/sig/request.rbs +7 -2
- data/sig/response.rbs +4 -1
- data/sig/session.rbs +4 -0
- metadata +21 -7
- data/lib/httpx/timeout.rb +0 -67
- data/sig/timeout.rbs +0 -29
@@ -4,20 +4,20 @@ module HTTPX
|
|
4
4
|
module Plugins
|
5
5
|
module Compression
|
6
6
|
module Deflate
|
7
|
-
def self.load_dependencies(
|
7
|
+
def self.load_dependencies(_klass)
|
8
8
|
require "stringio"
|
9
9
|
require "zlib"
|
10
|
-
klass.plugin(:"compression/gzip")
|
11
10
|
end
|
12
11
|
|
13
|
-
def self.configure(
|
14
|
-
|
12
|
+
def self.configure(klass)
|
13
|
+
klass.plugin(:"compression/gzip")
|
14
|
+
klass.default_options.encodings.register "deflate", self
|
15
15
|
end
|
16
16
|
|
17
17
|
module Deflater
|
18
18
|
module_function
|
19
19
|
|
20
|
-
def deflate(raw, buffer, chunk_size:)
|
20
|
+
def deflate(raw, buffer = "".b, chunk_size: 16_384)
|
21
21
|
deflater = Zlib::Deflate.new
|
22
22
|
while (chunk = raw.read(chunk_size))
|
23
23
|
compressed = deflater.deflate(chunk)
|
@@ -27,6 +27,7 @@ module HTTPX
|
|
27
27
|
last = deflater.finish
|
28
28
|
buffer << last
|
29
29
|
yield last if block_given?
|
30
|
+
buffer
|
30
31
|
ensure
|
31
32
|
deflater.close if deflater
|
32
33
|
end
|
@@ -10,8 +10,8 @@ module HTTPX
|
|
10
10
|
require "zlib"
|
11
11
|
end
|
12
12
|
|
13
|
-
def self.configure(
|
14
|
-
|
13
|
+
def self.configure(klass)
|
14
|
+
klass.default_options.encodings.register "gzip", self
|
15
15
|
end
|
16
16
|
|
17
17
|
class Deflater
|
@@ -19,7 +19,7 @@ module HTTPX
|
|
19
19
|
@compressed_chunk = "".b
|
20
20
|
end
|
21
21
|
|
22
|
-
def deflate(raw, buffer, chunk_size:)
|
22
|
+
def deflate(raw, buffer = "".b, chunk_size: 16_384)
|
23
23
|
gzip = Zlib::GzipWriter.new(self)
|
24
24
|
|
25
25
|
begin
|
@@ -38,6 +38,7 @@ module HTTPX
|
|
38
38
|
|
39
39
|
buffer << compressed
|
40
40
|
yield compressed if block_given?
|
41
|
+
buffer
|
41
42
|
end
|
42
43
|
|
43
44
|
private
|
@@ -20,13 +20,9 @@ module HTTPX
|
|
20
20
|
|
21
21
|
def self.extra_options(options)
|
22
22
|
Class.new(options.class) do
|
23
|
-
def_option(:cookies)
|
24
|
-
|
25
|
-
|
26
|
-
else
|
27
|
-
Jar.new(cookies)
|
28
|
-
end
|
29
|
-
end
|
23
|
+
def_option(:cookies, <<-OUT)
|
24
|
+
value.is_a?(#{Jar}) ? value : #{Jar}.new(value)
|
25
|
+
OUT
|
30
26
|
end.new(options)
|
31
27
|
end
|
32
28
|
|
@@ -14,11 +14,11 @@ module HTTPX
|
|
14
14
|
|
15
15
|
def self.extra_options(options)
|
16
16
|
Class.new(options.class) do
|
17
|
-
def_option(:digest)
|
18
|
-
raise Error, ":digest must be a Digest" unless
|
17
|
+
def_option(:digest, <<-OUT)
|
18
|
+
raise Error, ":digest must be a Digest" unless value.is_a?(#{Digest})
|
19
19
|
|
20
|
-
|
21
|
-
|
20
|
+
value
|
21
|
+
OUT
|
22
22
|
end.new(options)
|
23
23
|
end
|
24
24
|
|
@@ -29,7 +29,7 @@ module HTTPX
|
|
29
29
|
|
30
30
|
module InstanceMethods
|
31
31
|
def digest_authentication(user, password)
|
32
|
-
|
32
|
+
with(digest: Digest.new(user, password))
|
33
33
|
end
|
34
34
|
|
35
35
|
alias_method :digest_auth, :digest_authentication
|
data/lib/httpx/plugins/expect.rb
CHANGED
@@ -16,19 +16,19 @@ module HTTPX
|
|
16
16
|
|
17
17
|
def self.extra_options(options)
|
18
18
|
Class.new(options.class) do
|
19
|
-
def_option(:expect_timeout)
|
20
|
-
seconds = Integer(
|
19
|
+
def_option(:expect_timeout, <<-OUT)
|
20
|
+
seconds = Integer(value)
|
21
21
|
raise Error, ":expect_timeout must be positive" unless seconds.positive?
|
22
22
|
|
23
23
|
seconds
|
24
|
-
|
24
|
+
OUT
|
25
25
|
|
26
|
-
def_option(:expect_threshold_size)
|
27
|
-
bytes = Integer(
|
26
|
+
def_option(:expect_threshold_size, <<-OUT)
|
27
|
+
bytes = Integer(value)
|
28
28
|
raise Error, ":expect_threshold_size must be positive" unless bytes.positive?
|
29
29
|
|
30
30
|
bytes
|
31
|
-
|
31
|
+
OUT
|
32
32
|
end.new(options).merge(expect_timeout: EXPECT_TIMEOUT)
|
33
33
|
end
|
34
34
|
|
@@ -19,12 +19,12 @@ module HTTPX
|
|
19
19
|
|
20
20
|
def self.extra_options(options)
|
21
21
|
Class.new(options.class) do
|
22
|
-
def_option(:max_redirects)
|
23
|
-
num = Integer(
|
22
|
+
def_option(:max_redirects, <<-OUT)
|
23
|
+
num = Integer(value)
|
24
24
|
raise Error, ":max_redirects must be positive" if num.negative?
|
25
25
|
|
26
26
|
num
|
27
|
-
|
27
|
+
OUT
|
28
28
|
|
29
29
|
def_option(:follow_insecure_redirects)
|
30
30
|
end.new(options)
|
@@ -32,7 +32,7 @@ module HTTPX
|
|
32
32
|
|
33
33
|
module InstanceMethods
|
34
34
|
def max_redirects(n)
|
35
|
-
|
35
|
+
with(max_redirects: n.to_i)
|
36
36
|
end
|
37
37
|
|
38
38
|
private
|
@@ -0,0 +1,247 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
GRPCError = Class.new(Error) do
|
5
|
+
attr_reader :status, :details, :metadata
|
6
|
+
|
7
|
+
def initialize(status, details, metadata)
|
8
|
+
@status = status
|
9
|
+
@details = details
|
10
|
+
@metadata = metadata
|
11
|
+
super("GRPC error, code=#{status}, details=#{details}, metadata=#{metadata}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module Plugins
|
16
|
+
#
|
17
|
+
# This plugin adds DSL to build GRPC interfaces.
|
18
|
+
#
|
19
|
+
# https://gitlab.com/honeyryderchuck/httpx/wikis/GRPC
|
20
|
+
#
|
21
|
+
module GRPC
|
22
|
+
unless String.method_defined?(:underscore)
|
23
|
+
module StringExtensions
|
24
|
+
refine String do
|
25
|
+
def underscore
|
26
|
+
s = dup # Avoid mutating the argument, as it might be frozen.
|
27
|
+
s.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
28
|
+
s.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
29
|
+
s.tr!("-", "_")
|
30
|
+
s.downcase!
|
31
|
+
s
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
using StringExtensions
|
36
|
+
end
|
37
|
+
|
38
|
+
DEADLINE = 60
|
39
|
+
MARSHAL_METHOD = :encode
|
40
|
+
UNMARSHAL_METHOD = :decode
|
41
|
+
HEADERS = {
|
42
|
+
"content-type" => "application/grpc",
|
43
|
+
"te" => "trailers",
|
44
|
+
"accept" => "application/grpc",
|
45
|
+
# metadata fits here
|
46
|
+
# ex "foo-bin" => base64("bar")
|
47
|
+
}.freeze
|
48
|
+
|
49
|
+
class << self
|
50
|
+
def load_dependencies(*)
|
51
|
+
require "stringio"
|
52
|
+
require "google/protobuf"
|
53
|
+
require "httpx/plugins/grpc/message"
|
54
|
+
require "httpx/plugins/grpc/call"
|
55
|
+
end
|
56
|
+
|
57
|
+
def configure(klass)
|
58
|
+
klass.plugin(:persistent)
|
59
|
+
klass.plugin(:compression)
|
60
|
+
klass.plugin(:stream)
|
61
|
+
end
|
62
|
+
|
63
|
+
def extra_options(options)
|
64
|
+
Class.new(options.class) do
|
65
|
+
def_option(:grpc_service, <<-OUT)
|
66
|
+
String(value)
|
67
|
+
OUT
|
68
|
+
|
69
|
+
def_option(:grpc_compression, <<-OUT)
|
70
|
+
case value
|
71
|
+
when true, false
|
72
|
+
value
|
73
|
+
else
|
74
|
+
value.to_s
|
75
|
+
end
|
76
|
+
OUT
|
77
|
+
|
78
|
+
def_option(:grpc_rpcs, <<-OUT)
|
79
|
+
Hash[value]
|
80
|
+
OUT
|
81
|
+
|
82
|
+
def_option(:grpc_deadline, <<-OUT)
|
83
|
+
raise Error, ":grpc_deadline must be positive" unless value.positive?
|
84
|
+
|
85
|
+
value
|
86
|
+
OUT
|
87
|
+
|
88
|
+
def_option(:call_credentials, <<-OUT)
|
89
|
+
raise Error, ":call_credentials must respond to #call" unless value.respond_to?(:call)
|
90
|
+
|
91
|
+
value
|
92
|
+
OUT
|
93
|
+
end.new(options).merge(
|
94
|
+
fallback_protocol: "h2",
|
95
|
+
http2_settings: { wait_for_handshake: false },
|
96
|
+
grpc_rpcs: {}.freeze,
|
97
|
+
grpc_compression: false,
|
98
|
+
grpc_deadline: DEADLINE
|
99
|
+
)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
module ResponseMethods
|
104
|
+
attr_reader :trailing_metadata
|
105
|
+
|
106
|
+
def merge_headers(trailers)
|
107
|
+
@trailing_metadata = Hash[trailers]
|
108
|
+
super
|
109
|
+
end
|
110
|
+
|
111
|
+
def encoders
|
112
|
+
@options.encodings
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
module InstanceMethods
|
117
|
+
def with_channel_credentials(ca_path, key = nil, cert = nil, **ssl_opts)
|
118
|
+
ssl_params = {
|
119
|
+
**ssl_opts,
|
120
|
+
ca_file: ca_path,
|
121
|
+
}
|
122
|
+
if key
|
123
|
+
key = File.read(key) if File.file?(key)
|
124
|
+
ssl_params[:key] = OpenSSL::PKey.read(key)
|
125
|
+
end
|
126
|
+
|
127
|
+
if cert
|
128
|
+
cert = File.read(cert) if File.file?(cert)
|
129
|
+
ssl_params[:cert] = OpenSSL::X509::Certificate.new(cert)
|
130
|
+
end
|
131
|
+
|
132
|
+
with(ssl: ssl_params)
|
133
|
+
end
|
134
|
+
|
135
|
+
def rpc(rpc_name, input, output, **opts)
|
136
|
+
rpc_name = rpc_name.to_s
|
137
|
+
raise Error, "rpc #{rpc_name} already defined" if @options.grpc_rpcs.key?(rpc_name)
|
138
|
+
|
139
|
+
rpc_opts = {
|
140
|
+
deadline: @options.grpc_deadline,
|
141
|
+
}.merge(opts)
|
142
|
+
|
143
|
+
with(grpc_rpcs: @options.grpc_rpcs.merge(
|
144
|
+
rpc_name.underscore => [rpc_name, input, output, rpc_opts]
|
145
|
+
).freeze)
|
146
|
+
end
|
147
|
+
|
148
|
+
def build_stub(origin, service: nil, compression: false)
|
149
|
+
scheme = @options.ssl.empty? ? "http" : "https"
|
150
|
+
|
151
|
+
origin = URI.parse("#{scheme}://#{origin}")
|
152
|
+
|
153
|
+
with(origin: origin, grpc_service: service, grpc_compression: compression)
|
154
|
+
end
|
155
|
+
|
156
|
+
def execute(rpc_method, input,
|
157
|
+
deadline: DEADLINE,
|
158
|
+
metadata: nil,
|
159
|
+
**opts)
|
160
|
+
grpc_request = build_grpc_request(rpc_method, input, deadline: deadline, metadata: metadata, **opts)
|
161
|
+
response = request(grpc_request, **opts)
|
162
|
+
response.raise_for_status
|
163
|
+
GRPC::Call.new(response, opts)
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def rpc_execute(rpc_name, input, **opts)
|
169
|
+
rpc_name, input_enc, output_enc, rpc_opts = @options.grpc_rpcs[rpc_name.to_s] || raise(Error, "#{rpc_name}: undefined service")
|
170
|
+
|
171
|
+
exec_opts = rpc_opts.merge(opts)
|
172
|
+
|
173
|
+
marshal_method ||= exec_opts.delete(:marshal_method) || MARSHAL_METHOD
|
174
|
+
unmarshal_method ||= exec_opts.delete(:unmarshal_method) || UNMARSHAL_METHOD
|
175
|
+
|
176
|
+
messages = if input.respond_to?(:each)
|
177
|
+
Enumerator.new do |y|
|
178
|
+
input.each do |message|
|
179
|
+
y << input_enc.__send__(marshal_method, message)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
else
|
183
|
+
input_enc.marshal(input)
|
184
|
+
end
|
185
|
+
|
186
|
+
call = execute(rpc_name, messages, **exec_opts)
|
187
|
+
|
188
|
+
call.decoder = output_enc.method(unmarshal_method)
|
189
|
+
|
190
|
+
call
|
191
|
+
end
|
192
|
+
|
193
|
+
def build_grpc_request(rpc_method, input, deadline:, metadata: nil, **)
|
194
|
+
uri = @options.origin.dup
|
195
|
+
rpc_method = "/#{rpc_method}" unless rpc_method.start_with?("/")
|
196
|
+
rpc_method = "/#{@options.grpc_service}#{rpc_method}" if @options.grpc_service
|
197
|
+
uri.path = rpc_method
|
198
|
+
|
199
|
+
headers = HEADERS.merge(
|
200
|
+
"grpc-accept-encoding" => ["identity", *@options.encodings.registry.keys]
|
201
|
+
)
|
202
|
+
unless deadline == Float::INFINITY
|
203
|
+
# convert to milliseconds
|
204
|
+
deadline = (deadline * 1000.0).to_i
|
205
|
+
headers["grpc-timeout"] = "#{deadline}m"
|
206
|
+
end
|
207
|
+
|
208
|
+
headers = headers.merge(metadata) if metadata
|
209
|
+
|
210
|
+
# prepare compressor
|
211
|
+
deflater = nil
|
212
|
+
compression = @options.grpc_compression == true ? "gzip" : @options.grpc_compression
|
213
|
+
|
214
|
+
if compression
|
215
|
+
headers["grpc-encoding"] = compression
|
216
|
+
deflater = @options.encodings.registry(compression).deflater
|
217
|
+
end
|
218
|
+
|
219
|
+
headers.merge!(@options.call_credentials.call) if @options.call_credentials
|
220
|
+
|
221
|
+
body = if input.respond_to?(:each)
|
222
|
+
Enumerator.new do |y|
|
223
|
+
input.each do |message|
|
224
|
+
y << Message.encode(message, deflater: deflater)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
else
|
228
|
+
Message.encode(input, deflater: deflater)
|
229
|
+
end
|
230
|
+
|
231
|
+
build_request(:post, uri, headers: headers, body: body)
|
232
|
+
end
|
233
|
+
|
234
|
+
def respond_to_missing?(meth, *, &blk)
|
235
|
+
@options.grpc_rpcs.key?(meth.to_s) || super
|
236
|
+
end
|
237
|
+
|
238
|
+
def method_missing(meth, *args, **kwargs, &blk)
|
239
|
+
return rpc_execute(meth, *args, **kwargs, &blk) if @options.grpc_rpcs.key?(meth.to_s)
|
240
|
+
|
241
|
+
super
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
register_plugin :grpc, GRPC
|
246
|
+
end
|
247
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
module GRPC
|
6
|
+
# Encapsulates call information
|
7
|
+
class Call
|
8
|
+
attr_writer :decoder
|
9
|
+
|
10
|
+
def initialize(response, options)
|
11
|
+
@response = response
|
12
|
+
@options = options
|
13
|
+
@decoder = ->(z) { z }
|
14
|
+
end
|
15
|
+
|
16
|
+
def inspect
|
17
|
+
"#GRPC::Call(#{grpc_response})"
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
grpc_response.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
def metadata
|
25
|
+
response.headers
|
26
|
+
end
|
27
|
+
|
28
|
+
def trailing_metadata
|
29
|
+
return unless @response.body.closed?
|
30
|
+
|
31
|
+
@response.trailing_metadata
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def grpc_response
|
37
|
+
return @grpc_response if defined?(@grpc_response)
|
38
|
+
|
39
|
+
@grpc_response = if @response.respond_to?(:each)
|
40
|
+
Enumerator.new do |y|
|
41
|
+
Message.stream(@response).each do |message|
|
42
|
+
y << @decoder.call(message)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
else
|
46
|
+
@decoder.call(Message.unary(@response))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def respond_to_missing?(meth, *args, &blk)
|
51
|
+
grpc_response.respond_to?(meth, *args) || super
|
52
|
+
end
|
53
|
+
|
54
|
+
def method_missing(meth, *args, &blk)
|
55
|
+
return grpc_response.__send__(meth, *args, &blk) if grpc_response.respond_to?(meth)
|
56
|
+
|
57
|
+
super
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|