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.
Files changed (53) 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 +1 -1
  5. data/doc/release_notes/0_14_0.md +79 -0
  6. data/lib/httpx.rb +1 -2
  7. data/lib/httpx/callbacks.rb +12 -3
  8. data/lib/httpx/connection.rb +12 -9
  9. data/lib/httpx/connection/http1.rb +26 -11
  10. data/lib/httpx/connection/http2.rb +52 -8
  11. data/lib/httpx/headers.rb +1 -1
  12. data/lib/httpx/io/tcp.rb +1 -1
  13. data/lib/httpx/options.rb +91 -56
  14. data/lib/httpx/plugins/aws_sdk_authentication.rb +5 -2
  15. data/lib/httpx/plugins/aws_sigv4.rb +4 -4
  16. data/lib/httpx/plugins/basic_authentication.rb +8 -3
  17. data/lib/httpx/plugins/compression.rb +8 -8
  18. data/lib/httpx/plugins/compression/brotli.rb +4 -3
  19. data/lib/httpx/plugins/compression/deflate.rb +4 -3
  20. data/lib/httpx/plugins/compression/gzip.rb +2 -1
  21. data/lib/httpx/plugins/cookies.rb +3 -7
  22. data/lib/httpx/plugins/digest_authentication.rb +4 -4
  23. data/lib/httpx/plugins/expect.rb +6 -6
  24. data/lib/httpx/plugins/follow_redirects.rb +3 -3
  25. data/lib/httpx/plugins/grpc.rb +247 -0
  26. data/lib/httpx/plugins/grpc/call.rb +62 -0
  27. data/lib/httpx/plugins/grpc/message.rb +85 -0
  28. data/lib/httpx/plugins/multipart/part.rb +1 -1
  29. data/lib/httpx/plugins/proxy.rb +3 -7
  30. data/lib/httpx/plugins/proxy/ssh.rb +3 -3
  31. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  32. data/lib/httpx/plugins/retries.rb +13 -14
  33. data/lib/httpx/plugins/stream.rb +96 -74
  34. data/lib/httpx/plugins/upgrade.rb +4 -4
  35. data/lib/httpx/request.rb +25 -2
  36. data/lib/httpx/response.rb +4 -0
  37. data/lib/httpx/session.rb +17 -7
  38. data/lib/httpx/transcoder/chunker.rb +1 -1
  39. data/lib/httpx/version.rb +1 -1
  40. data/sig/callbacks.rbs +2 -0
  41. data/sig/connection/http1.rbs +4 -0
  42. data/sig/connection/http2.rbs +5 -1
  43. data/sig/options.rbs +9 -2
  44. data/sig/plugins/aws_sdk_authentication.rbs +2 -0
  45. data/sig/plugins/basic_authentication.rbs +2 -0
  46. data/sig/plugins/compression.rbs +2 -2
  47. data/sig/plugins/stream.rbs +17 -16
  48. data/sig/request.rbs +7 -2
  49. data/sig/response.rbs +1 -0
  50. data/sig/session.rbs +4 -0
  51. metadata +38 -35
  52. data/lib/httpx/timeout.rb +0 -67
  53. 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)
@@ -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) do |pr|
71
- if pr.is_a?(Parameters)
72
- pr
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
@@ -12,9 +12,9 @@ module HTTPX
12
12
 
13
13
  def self.extra_options(options)
14
14
  Class.new(options.class) do
15
- def_option(:proxy) do |pr|
16
- Hash[pr]
17
- end
15
+ def_option(:proxy, <<-OUT)
16
+ Hash[value]
17
+ OUT
18
18
  end.new(options)
19
19
  end
20
20
 
@@ -15,7 +15,7 @@ module HTTPX
15
15
  class << self
16
16
  RATE_LIMIT_CODES = [429, 503].freeze
17
17
 
18
- def load_dependencies(klass)
18
+ def configure(klass)
19
19
  klass.plugin(:retries,
20
20
  retry_change_requests: true,
21
21
  retry_on: method(:retry_on_rate_limited_response),