httpx 0.13.2 → 0.14.4

Sign up to get free protection for your applications and to get access to all the features.
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 +1 -1
  5. data/doc/release_notes/0_14_0.md +79 -0
  6. data/doc/release_notes/0_14_1.md +7 -0
  7. data/doc/release_notes/0_14_2.md +6 -0
  8. data/doc/release_notes/0_14_3.md +5 -0
  9. data/doc/release_notes/0_14_4.md +5 -0
  10. data/lib/httpx.rb +1 -2
  11. data/lib/httpx/callbacks.rb +12 -3
  12. data/lib/httpx/connection.rb +12 -9
  13. data/lib/httpx/connection/http1.rb +40 -15
  14. data/lib/httpx/connection/http2.rb +61 -15
  15. data/lib/httpx/headers.rb +7 -3
  16. data/lib/httpx/idna.rb +15 -0
  17. data/lib/httpx/io/tcp.rb +1 -1
  18. data/lib/httpx/options.rb +91 -56
  19. data/lib/httpx/plugins/aws_sdk_authentication.rb +5 -2
  20. data/lib/httpx/plugins/aws_sigv4.rb +4 -4
  21. data/lib/httpx/plugins/basic_authentication.rb +8 -3
  22. data/lib/httpx/plugins/compression.rb +8 -8
  23. data/lib/httpx/plugins/compression/brotli.rb +4 -3
  24. data/lib/httpx/plugins/compression/deflate.rb +4 -3
  25. data/lib/httpx/plugins/compression/gzip.rb +2 -1
  26. data/lib/httpx/plugins/cookies.rb +3 -7
  27. data/lib/httpx/plugins/digest_authentication.rb +4 -4
  28. data/lib/httpx/plugins/expect.rb +6 -6
  29. data/lib/httpx/plugins/follow_redirects.rb +3 -3
  30. data/lib/httpx/plugins/grpc.rb +247 -0
  31. data/lib/httpx/plugins/grpc/call.rb +62 -0
  32. data/lib/httpx/plugins/grpc/message.rb +85 -0
  33. data/lib/httpx/plugins/multipart/part.rb +2 -2
  34. data/lib/httpx/plugins/proxy.rb +3 -7
  35. data/lib/httpx/plugins/proxy/http.rb +5 -4
  36. data/lib/httpx/plugins/proxy/ssh.rb +3 -3
  37. data/lib/httpx/plugins/rate_limiter.rb +1 -1
  38. data/lib/httpx/plugins/retries.rb +13 -14
  39. data/lib/httpx/plugins/stream.rb +96 -74
  40. data/lib/httpx/plugins/upgrade.rb +4 -4
  41. data/lib/httpx/request.rb +25 -2
  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 +19 -7
  60. data/lib/httpx/timeout.rb +0 -67
  61. data/sig/timeout.rbs +0 -29
@@ -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
@@ -19,14 +19,14 @@ 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)
26
26
  content_type ||= MimeTypeDetector.call(value, filename) || "application/octet-stream"
27
27
  [value, content_type, filename]
28
28
  else
29
- [StringIO.new(value.to_s), "text/plain"]
29
+ [StringIO.new(value.to_s), content_type || "text/plain", filename]
30
30
  end
31
31
  end
32
32
  end