httpx 0.13.0 → 0.14.2

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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_10_1.md +1 -1
  3. data/doc/release_notes/0_13_0.md +2 -2
  4. data/doc/release_notes/0_13_1.md +5 -0
  5. data/doc/release_notes/0_13_2.md +9 -0
  6. data/doc/release_notes/0_14_0.md +79 -0
  7. data/doc/release_notes/0_14_1.md +7 -0
  8. data/doc/release_notes/0_14_2.md +6 -0
  9. data/lib/httpx.rb +1 -2
  10. data/lib/httpx/callbacks.rb +12 -3
  11. data/lib/httpx/connection.rb +12 -9
  12. data/lib/httpx/connection/http1.rb +32 -14
  13. data/lib/httpx/connection/http2.rb +61 -15
  14. data/lib/httpx/headers.rb +7 -3
  15. data/lib/httpx/io/tcp.rb +3 -1
  16. data/lib/httpx/io/udp.rb +31 -7
  17. data/lib/httpx/options.rb +91 -56
  18. data/lib/httpx/plugins/aws_sdk_authentication.rb +5 -2
  19. data/lib/httpx/plugins/aws_sigv4.rb +5 -4
  20. data/lib/httpx/plugins/basic_authentication.rb +8 -3
  21. data/lib/httpx/plugins/compression.rb +8 -8
  22. data/lib/httpx/plugins/compression/brotli.rb +4 -3
  23. data/lib/httpx/plugins/compression/deflate.rb +4 -3
  24. data/lib/httpx/plugins/compression/gzip.rb +2 -1
  25. data/lib/httpx/plugins/cookies.rb +3 -7
  26. data/lib/httpx/plugins/digest_authentication.rb +4 -4
  27. data/lib/httpx/plugins/expect.rb +6 -6
  28. data/lib/httpx/plugins/follow_redirects.rb +3 -3
  29. data/lib/httpx/plugins/grpc.rb +247 -0
  30. data/lib/httpx/plugins/grpc/call.rb +62 -0
  31. data/lib/httpx/plugins/grpc/message.rb +85 -0
  32. data/lib/httpx/plugins/multipart/part.rb +2 -2
  33. data/lib/httpx/plugins/proxy.rb +3 -7
  34. data/lib/httpx/plugins/proxy/http.rb +5 -4
  35. data/lib/httpx/plugins/proxy/ssh.rb +3 -3
  36. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  37. data/lib/httpx/plugins/retries.rb +13 -14
  38. data/lib/httpx/plugins/stream.rb +96 -74
  39. data/lib/httpx/plugins/upgrade.rb +6 -5
  40. data/lib/httpx/request.rb +25 -2
  41. data/lib/httpx/resolver/native.rb +7 -3
  42. data/lib/httpx/response.rb +4 -0
  43. data/lib/httpx/session.rb +17 -7
  44. data/lib/httpx/transcoder/chunker.rb +1 -1
  45. data/lib/httpx/version.rb +1 -1
  46. data/sig/callbacks.rbs +2 -0
  47. data/sig/connection/http1.rbs +5 -1
  48. data/sig/connection/http2.rbs +6 -2
  49. data/sig/headers.rbs +2 -2
  50. data/sig/options.rbs +9 -2
  51. data/sig/plugins/aws_sdk_authentication.rbs +2 -0
  52. data/sig/plugins/basic_authentication.rbs +2 -0
  53. data/sig/plugins/compression.rbs +2 -2
  54. data/sig/plugins/multipart.rbs +1 -1
  55. data/sig/plugins/stream.rbs +17 -16
  56. data/sig/request.rbs +7 -2
  57. data/sig/response.rbs +1 -0
  58. data/sig/session.rbs +4 -0
  59. metadata +18 -7
  60. data/lib/httpx/timeout.rb +0 -67
  61. data/sig/timeout.rbs +0 -29
@@ -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) do |cookies|
24
- if cookies.is_a?(Jar)
25
- cookies
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) do |digest|
18
- raise Error, ":digest must be a Digest" unless digest.is_a?(Digest)
17
+ def_option(:digest, <<-OUT)
18
+ raise Error, ":digest must be a Digest" unless value.is_a?(#{Digest})
19
19
 
20
- digest
21
- end
20
+ value
21
+ OUT
22
22
  end.new(options)
23
23
  end
24
24
 
@@ -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) do |seconds|
20
- seconds = Integer(seconds)
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
- end
24
+ OUT
25
25
 
26
- def_option(:expect_threshold_size) do |bytes|
27
- bytes = Integer(bytes)
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
- end
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) do |num|
23
- num = Integer(num)
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
- end
27
+ OUT
28
28
 
29
29
  def_option(:follow_insecure_redirects)
30
30
  end.new(options)
@@ -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