httpx 0.13.2 → 0.14.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_10_1.md +1 -1
- data/doc/release_notes/0_13_0.md +2 -2
- data/doc/release_notes/0_13_1.md +1 -1
- data/doc/release_notes/0_14_0.md +79 -0
- data/lib/httpx.rb +1 -2
- data/lib/httpx/callbacks.rb +12 -3
- data/lib/httpx/connection.rb +12 -9
- data/lib/httpx/connection/http1.rb +26 -11
- data/lib/httpx/connection/http2.rb +52 -8
- data/lib/httpx/headers.rb +1 -1
- data/lib/httpx/io/tcp.rb +1 -1
- data/lib/httpx/options.rb +91 -56
- data/lib/httpx/plugins/aws_sdk_authentication.rb +5 -2
- data/lib/httpx/plugins/aws_sigv4.rb +4 -4
- data/lib/httpx/plugins/basic_authentication.rb +8 -3
- data/lib/httpx/plugins/compression.rb +8 -8
- data/lib/httpx/plugins/compression/brotli.rb +4 -3
- data/lib/httpx/plugins/compression/deflate.rb +4 -3
- data/lib/httpx/plugins/compression/gzip.rb +2 -1
- data/lib/httpx/plugins/cookies.rb +3 -7
- data/lib/httpx/plugins/digest_authentication.rb +4 -4
- data/lib/httpx/plugins/expect.rb +6 -6
- data/lib/httpx/plugins/follow_redirects.rb +3 -3
- 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/multipart/part.rb +1 -1
- data/lib/httpx/plugins/proxy.rb +3 -7
- data/lib/httpx/plugins/proxy/ssh.rb +3 -3
- data/lib/httpx/plugins/rate_limiter.rb +1 -1
- data/lib/httpx/plugins/retries.rb +13 -14
- data/lib/httpx/plugins/stream.rb +96 -74
- data/lib/httpx/plugins/upgrade.rb +4 -4
- data/lib/httpx/request.rb +25 -2
- data/lib/httpx/response.rb +4 -0
- 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/connection/http1.rbs +4 -0
- data/sig/connection/http2.rbs +5 -1
- data/sig/options.rbs +9 -2
- data/sig/plugins/aws_sdk_authentication.rbs +2 -0
- data/sig/plugins/basic_authentication.rbs +2 -0
- data/sig/plugins/compression.rbs +2 -2
- data/sig/plugins/stream.rbs +17 -16
- data/sig/request.rbs +7 -2
- data/sig/response.rbs +1 -0
- data/sig/session.rbs +4 -0
- metadata +38 -35
- data/lib/httpx/timeout.rb +0 -67
- data/sig/timeout.rbs +0 -29
@@ -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
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTTPX
|
4
|
+
module Plugins
|
5
|
+
module GRPC
|
6
|
+
# Encoding module for GRPC responses
|
7
|
+
#
|
8
|
+
# Can encode and decode grpc messages.
|
9
|
+
module Message
|
10
|
+
module_function
|
11
|
+
|
12
|
+
# decodes a unary grpc response
|
13
|
+
def unary(response)
|
14
|
+
verify_status(response)
|
15
|
+
decode(response.to_s, encodings: response.headers.get("grpc-encoding"), encoders: response.encoders)
|
16
|
+
end
|
17
|
+
|
18
|
+
# lazy decodes a grpc stream response
|
19
|
+
def stream(response, &block)
|
20
|
+
return enum_for(__method__, response) unless block_given?
|
21
|
+
|
22
|
+
response.each do |frame|
|
23
|
+
decode(frame, encodings: response.headers.get("grpc-encoding"), encoders: response.encoders, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
verify_status(response)
|
27
|
+
end
|
28
|
+
|
29
|
+
# encodes a single grpc message
|
30
|
+
def encode(bytes, deflater:)
|
31
|
+
if deflater
|
32
|
+
compressed_flag = 1
|
33
|
+
bytes = deflater.deflate(StringIO.new(bytes))
|
34
|
+
else
|
35
|
+
compressed_flag = 0
|
36
|
+
end
|
37
|
+
|
38
|
+
"".b << [compressed_flag, bytes.bytesize].pack("CL>") << bytes.to_s
|
39
|
+
end
|
40
|
+
|
41
|
+
# decodes a single grpc message
|
42
|
+
def decode(message, encodings:, encoders:)
|
43
|
+
until message.empty?
|
44
|
+
|
45
|
+
compressed, size = message.unpack("CL>")
|
46
|
+
|
47
|
+
data = message.byteslice(5..size + 5 - 1)
|
48
|
+
if compressed == 1
|
49
|
+
encodings.reverse_each do |algo|
|
50
|
+
inflater = encoders.registry(algo).inflater(size)
|
51
|
+
data = inflater.inflate(data)
|
52
|
+
size = data.bytesize
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
return data unless block_given?
|
57
|
+
|
58
|
+
yield data
|
59
|
+
|
60
|
+
message = message.byteslice(5 + size..-1)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def cancel(request)
|
65
|
+
request.emit(:refuse, :client_cancellation)
|
66
|
+
end
|
67
|
+
|
68
|
+
# interprets the grpc call trailing metadata, and raises an
|
69
|
+
# exception in case of error code
|
70
|
+
def verify_status(response)
|
71
|
+
# return standard errors if need be
|
72
|
+
response.raise_for_status
|
73
|
+
|
74
|
+
status = Integer(response.headers["grpc-status"])
|
75
|
+
message = response.headers["grpc-message"]
|
76
|
+
|
77
|
+
return if status.zero?
|
78
|
+
|
79
|
+
response.close
|
80
|
+
raise GRPCError.new(status, message, response.trailing_metadata)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -19,7 +19,7 @@ module HTTPX
|
|
19
19
|
value = value[:body]
|
20
20
|
end
|
21
21
|
|
22
|
-
value = value.open(:binmode => true) if value.is_a?(Pathname)
|
22
|
+
value = value.open(:binmode => true) if Object.const_defined?(:Pathname) && value.is_a?(Pathname)
|
23
23
|
|
24
24
|
if value.is_a?(File)
|
25
25
|
filename ||= File.basename(value.path)
|
data/lib/httpx/plugins/proxy.rb
CHANGED
@@ -67,13 +67,9 @@ module HTTPX
|
|
67
67
|
|
68
68
|
def extra_options(options)
|
69
69
|
Class.new(options.class) do
|
70
|
-
def_option(:proxy)
|
71
|
-
|
72
|
-
|
73
|
-
else
|
74
|
-
Hash[pr]
|
75
|
-
end
|
76
|
-
end
|
70
|
+
def_option(:proxy, <<-OUT)
|
71
|
+
value.is_a?(#{Parameters}) ? value : Hash[value]
|
72
|
+
OUT
|
77
73
|
end.new(options)
|
78
74
|
end
|
79
75
|
end
|