httpx 0.15.4 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_16_0.md +93 -0
  3. data/doc/release_notes/0_16_1.md +5 -0
  4. data/doc/release_notes/0_17_0.md +49 -0
  5. data/doc/release_notes/0_18_0.md +69 -0
  6. data/lib/httpx/adapters/datadog.rb +1 -1
  7. data/lib/httpx/adapters/faraday.rb +8 -14
  8. data/lib/httpx/adapters/webmock.rb +9 -3
  9. data/lib/httpx/altsvc.rb +2 -2
  10. data/lib/httpx/buffer.rb +1 -1
  11. data/lib/httpx/callbacks.rb +1 -1
  12. data/lib/httpx/chainable.rb +18 -11
  13. data/lib/httpx/connection/http1.rb +21 -13
  14. data/lib/httpx/connection/http2.rb +20 -25
  15. data/lib/httpx/connection.rb +73 -77
  16. data/lib/httpx/domain_name.rb +1 -1
  17. data/lib/httpx/errors.rb +11 -11
  18. data/lib/httpx/extensions.rb +50 -4
  19. data/lib/httpx/headers.rb +1 -1
  20. data/lib/httpx/io/ssl.rb +3 -3
  21. data/lib/httpx/io/tls.rb +8 -8
  22. data/lib/httpx/loggable.rb +5 -5
  23. data/lib/httpx/options.rb +108 -81
  24. data/lib/httpx/parser/http1.rb +11 -7
  25. data/lib/httpx/plugins/aws_sdk_authentication.rb +42 -18
  26. data/lib/httpx/plugins/aws_sigv4.rb +19 -20
  27. data/lib/httpx/plugins/compression.rb +17 -14
  28. data/lib/httpx/plugins/cookies/cookie.rb +4 -2
  29. data/lib/httpx/plugins/cookies/jar.rb +21 -2
  30. data/lib/httpx/plugins/cookies.rb +20 -7
  31. data/lib/httpx/plugins/digest_authentication.rb +19 -15
  32. data/lib/httpx/plugins/expect.rb +26 -18
  33. data/lib/httpx/plugins/follow_redirects.rb +9 -9
  34. data/lib/httpx/plugins/grpc/call.rb +4 -1
  35. data/lib/httpx/plugins/grpc/message.rb +2 -2
  36. data/lib/httpx/plugins/grpc.rb +72 -46
  37. data/lib/httpx/plugins/h2c.rb +7 -3
  38. data/lib/httpx/plugins/internal_telemetry.rb +8 -8
  39. data/lib/httpx/plugins/multipart/decoder.rb +187 -0
  40. data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
  41. data/lib/httpx/plugins/multipart/part.rb +2 -2
  42. data/lib/httpx/plugins/multipart.rb +16 -2
  43. data/lib/httpx/plugins/ntlm_authentication.rb +12 -10
  44. data/lib/httpx/plugins/proxy/socks4.rb +2 -1
  45. data/lib/httpx/plugins/proxy/socks5.rb +2 -1
  46. data/lib/httpx/plugins/proxy/ssh.rb +20 -13
  47. data/lib/httpx/plugins/proxy.rb +10 -10
  48. data/lib/httpx/plugins/response_cache/store.rb +55 -0
  49. data/lib/httpx/plugins/response_cache.rb +88 -0
  50. data/lib/httpx/plugins/retries.rb +46 -23
  51. data/lib/httpx/plugins/stream.rb +3 -4
  52. data/lib/httpx/plugins/upgrade.rb +7 -6
  53. data/lib/httpx/pool.rb +39 -13
  54. data/lib/httpx/registry.rb +2 -2
  55. data/lib/httpx/request.rb +16 -25
  56. data/lib/httpx/resolver/https.rb +4 -8
  57. data/lib/httpx/resolver/native.rb +19 -5
  58. data/lib/httpx/resolver/resolver_mixin.rb +2 -1
  59. data/lib/httpx/resolver/system.rb +2 -0
  60. data/lib/httpx/resolver.rb +2 -2
  61. data/lib/httpx/response.rb +91 -48
  62. data/lib/httpx/selector.rb +11 -24
  63. data/lib/httpx/session.rb +41 -23
  64. data/lib/httpx/session2.rb +23 -0
  65. data/lib/httpx/timers.rb +84 -0
  66. data/lib/httpx/transcoder/body.rb +3 -2
  67. data/lib/httpx/transcoder/chunker.rb +2 -1
  68. data/lib/httpx/transcoder/form.rb +20 -0
  69. data/lib/httpx/transcoder/json.rb +12 -0
  70. data/lib/httpx/transcoder.rb +62 -1
  71. data/lib/httpx/utils.rb +10 -2
  72. data/lib/httpx/version.rb +1 -1
  73. data/lib/httpx.rb +7 -3
  74. data/sig/buffer.rbs +3 -1
  75. data/sig/chainable.rbs +31 -29
  76. data/sig/connection/http1.rbs +11 -5
  77. data/sig/connection/http2.rbs +16 -5
  78. data/sig/connection.rbs +31 -13
  79. data/sig/errors.rbs +35 -1
  80. data/sig/headers.rbs +20 -19
  81. data/sig/httpx.rbs +4 -1
  82. data/sig/loggable.rbs +3 -1
  83. data/sig/options.rbs +45 -34
  84. data/sig/parser/http1.rbs +3 -3
  85. data/sig/plugins/authentication.rbs +1 -1
  86. data/sig/plugins/aws_sdk_authentication.rbs +25 -3
  87. data/sig/plugins/aws_sigv4.rbs +13 -5
  88. data/sig/plugins/basic_authentication.rbs +1 -1
  89. data/sig/plugins/compression.rbs +4 -6
  90. data/sig/plugins/cookies/cookie.rbs +5 -7
  91. data/sig/plugins/cookies/jar.rbs +9 -10
  92. data/sig/plugins/cookies.rbs +4 -5
  93. data/sig/plugins/digest_authentication.rbs +2 -3
  94. data/sig/plugins/expect.rbs +2 -4
  95. data/sig/plugins/follow_redirects.rbs +3 -5
  96. data/sig/plugins/grpc.rbs +4 -7
  97. data/sig/plugins/h2c.rbs +0 -2
  98. data/sig/plugins/multipart.rbs +64 -10
  99. data/sig/plugins/ntlm_authentication.rbs +2 -3
  100. data/sig/plugins/persistent.rbs +3 -8
  101. data/sig/plugins/proxy/ssh.rbs +4 -4
  102. data/sig/plugins/proxy.rbs +13 -13
  103. data/sig/plugins/push_promise.rbs +0 -2
  104. data/sig/plugins/response_cache.rbs +35 -0
  105. data/sig/plugins/retries.rbs +7 -8
  106. data/sig/plugins/stream.rbs +1 -1
  107. data/sig/plugins/upgrade.rbs +2 -3
  108. data/sig/pool.rbs +7 -2
  109. data/sig/registry.rbs +1 -1
  110. data/sig/request.rbs +11 -8
  111. data/sig/resolver/native.rbs +10 -5
  112. data/sig/resolver/resolver_mixin.rbs +4 -5
  113. data/sig/resolver/system.rbs +4 -0
  114. data/sig/resolver.rbs +7 -0
  115. data/sig/response.rbs +26 -13
  116. data/sig/selector.rbs +11 -9
  117. data/sig/session.rbs +22 -23
  118. data/sig/timers.rbs +32 -0
  119. data/sig/transcoder/body.rbs +6 -1
  120. data/sig/transcoder/chunker.rbs +8 -2
  121. data/sig/transcoder/form.rbs +3 -1
  122. data/sig/transcoder/json.rbs +2 -0
  123. data/sig/transcoder.rbs +13 -5
  124. data/sig/utils.rbs +6 -0
  125. metadata +18 -18
  126. data/lib/httpx/request2.rb +0 -14
@@ -14,19 +14,23 @@ module HTTPX
14
14
 
15
15
  DigestError = Class.new(Error)
16
16
 
17
- def self.extra_options(options)
18
- Class.new(options.class) do
19
- def_option(:digest, <<-OUT)
20
- raise Error, ":digest must be a Digest" unless value.is_a?(#{Digest})
21
-
22
- value
23
- OUT
24
- end.new(options).merge(max_concurrent_requests: 1)
17
+ class << self
18
+ def extra_options(options)
19
+ options.merge(max_concurrent_requests: 1)
20
+ end
21
+
22
+ def load_dependencies(*)
23
+ require "securerandom"
24
+ require "digest"
25
+ end
25
26
  end
26
27
 
27
- def self.load_dependencies(*)
28
- require "securerandom"
29
- require "digest"
28
+ module OptionsMethods
29
+ def option_digest(value)
30
+ raise TypeError, ":digest must be a Digest" unless value.is_a?(Digest)
31
+
32
+ value
33
+ end
30
34
  end
31
35
 
32
36
  module InstanceMethods
@@ -36,12 +40,12 @@ module HTTPX
36
40
 
37
41
  alias_method :digest_auth, :digest_authentication
38
42
 
39
- def send_requests(*requests, options)
43
+ def send_requests(*requests)
40
44
  requests.flat_map do |request|
41
45
  digest = request.options.digest
42
46
 
43
47
  if digest
44
- probe_response = wrap { super(request, options).first }
48
+ probe_response = wrap { super(request).first }
45
49
 
46
50
  if digest && !probe_response.is_a?(ErrorResponse) &&
47
51
  probe_response.status == 401 && probe_response.headers.key?("www-authenticate") &&
@@ -52,12 +56,12 @@ module HTTPX
52
56
  token = digest.generate_header(request, probe_response)
53
57
  request.headers["authorization"] = "Digest #{token}"
54
58
 
55
- super(request, options)
59
+ super(request)
56
60
  else
57
61
  probe_response
58
62
  end
59
63
  else
60
- super(request, options)
64
+ super(request)
61
65
  end
62
66
  end
63
67
  end
@@ -10,26 +10,30 @@ module HTTPX
10
10
  module Expect
11
11
  EXPECT_TIMEOUT = 2
12
12
 
13
- def self.no_expect_store
14
- @no_expect_store ||= []
13
+ class << self
14
+ def no_expect_store
15
+ @no_expect_store ||= []
16
+ end
17
+
18
+ def extra_options(options)
19
+ options.merge(expect_timeout: EXPECT_TIMEOUT)
20
+ end
15
21
  end
16
22
 
17
- def self.extra_options(options)
18
- Class.new(options.class) do
19
- def_option(:expect_timeout, <<-OUT)
20
- seconds = Integer(value)
21
- raise Error, ":expect_timeout must be positive" unless seconds.positive?
23
+ module OptionsMethods
24
+ def option_expect_timeout(value)
25
+ seconds = Integer(value)
26
+ raise TypeError, ":expect_timeout must be positive" unless seconds.positive?
22
27
 
23
- seconds
24
- OUT
28
+ seconds
29
+ end
25
30
 
26
- def_option(:expect_threshold_size, <<-OUT)
27
- bytes = Integer(value)
28
- raise Error, ":expect_threshold_size must be positive" unless bytes.positive?
31
+ def option_expect_threshold_size(value)
32
+ bytes = Integer(value)
33
+ raise TypeError, ":expect_threshold_size must be positive" unless bytes.positive?
29
34
 
30
- bytes
31
- OUT
32
- end.new(options).merge(expect_timeout: EXPECT_TIMEOUT)
35
+ bytes
36
+ end
33
37
  end
34
38
 
35
39
  module RequestMethods
@@ -65,9 +69,14 @@ module HTTPX
65
69
  end
66
70
 
67
71
  module ConnectionMethods
68
- def send(request)
72
+ def send_request_to_parser(request)
73
+ super
74
+
75
+ return unless request.headers["expect"] == "100-continue"
76
+
69
77
  request.once(:expect) do
70
- @timers.after(@options.expect_timeout) do
78
+ @timers.after(request.options.expect_timeout) do
79
+ # expect timeout expired
71
80
  if request.state == :expect && !request.expects?
72
81
  Expect.no_expect_store << request.origin
73
82
  request.headers.delete("expect")
@@ -75,7 +84,6 @@ module HTTPX
75
84
  end
76
85
  end
77
86
  end
78
- super
79
87
  end
80
88
  end
81
89
 
@@ -17,17 +17,17 @@ module HTTPX
17
17
  MAX_REDIRECTS = 3
18
18
  REDIRECT_STATUS = (300..399).freeze
19
19
 
20
- def self.extra_options(options)
21
- Class.new(options.class) do
22
- def_option(:max_redirects, <<-OUT)
23
- num = Integer(value)
24
- raise Error, ":max_redirects must be positive" if num.negative?
20
+ module OptionsMethods
21
+ def option_max_redirects(value)
22
+ num = Integer(value)
23
+ raise TypeError, ":max_redirects must be positive" if num.negative?
25
24
 
26
- num
27
- OUT
25
+ num
26
+ end
28
27
 
29
- def_option(:follow_insecure_redirects)
30
- end.new(options)
28
+ def option_follow_insecure_redirects(value)
29
+ value
30
+ end
31
31
  end
32
32
 
33
33
  module InstanceMethods
@@ -10,6 +10,7 @@ module HTTPX
10
10
  def initialize(response)
11
11
  @response = response
12
12
  @decoder = ->(z) { z }
13
+ @consumed = false
13
14
  end
14
15
 
15
16
  def inspect
@@ -25,7 +26,7 @@ module HTTPX
25
26
  end
26
27
 
27
28
  def trailing_metadata
28
- return unless @response.body.closed?
29
+ return unless @consumed
29
30
 
30
31
  @response.trailing_metadata
31
32
  end
@@ -40,8 +41,10 @@ module HTTPX
40
41
  Message.stream(@response).each do |message|
41
42
  y << @decoder.call(message)
42
43
  end
44
+ @consumed = true
43
45
  end
44
46
  else
47
+ @consumed = true
45
48
  @decoder.call(Message.unary(@response))
46
49
  end
47
50
  end
@@ -17,7 +17,7 @@ module HTTPX
17
17
 
18
18
  # lazy decodes a grpc stream response
19
19
  def stream(response, &block)
20
- return enum_for(__method__, response) unless block_given?
20
+ return enum_for(__method__, response) unless block
21
21
 
22
22
  response.each do |frame|
23
23
  decode(frame, encodings: response.headers.get("grpc-encoding"), encoders: response.encoders, &block)
@@ -57,7 +57,7 @@ module HTTPX
57
57
 
58
58
  yield data
59
59
 
60
- message = message.byteslice(5 + size..-1)
60
+ message = message.byteslice((5 + size)..-1)
61
61
  end
62
62
  end
63
63
 
@@ -49,7 +49,6 @@ module HTTPX
49
49
  class << self
50
50
  def load_dependencies(*)
51
51
  require "stringio"
52
- require "google/protobuf"
53
52
  require "httpx/plugins/grpc/message"
54
53
  require "httpx/plugins/grpc/call"
55
54
  end
@@ -61,36 +60,7 @@ module HTTPX
61
60
  end
62
61
 
63
62
  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(
63
+ options.merge(
94
64
  fallback_protocol: "h2",
95
65
  http2_settings: { wait_for_handshake: false },
96
66
  grpc_rpcs: {}.freeze,
@@ -100,6 +70,37 @@ module HTTPX
100
70
  end
101
71
  end
102
72
 
73
+ module OptionsMethods
74
+ def option_grpc_service(value)
75
+ String(value)
76
+ end
77
+
78
+ def option_grpc_compression(value)
79
+ case value
80
+ when true, false
81
+ value
82
+ else
83
+ value.to_s
84
+ end
85
+ end
86
+
87
+ def option_grpc_rpcs(value)
88
+ Hash[value]
89
+ end
90
+
91
+ def option_grpc_deadline(value)
92
+ raise TypeError, ":grpc_deadline must be positive" unless value.positive?
93
+
94
+ value
95
+ end
96
+
97
+ def option_call_credentials(value)
98
+ raise TypeError, ":call_credentials must respond to #call" unless value.respond_to?(:call)
99
+
100
+ value
101
+ end
102
+ end
103
+
103
104
  module ResponseMethods
104
105
  attr_reader :trailing_metadata
105
106
 
@@ -140,9 +141,19 @@ module HTTPX
140
141
  deadline: @options.grpc_deadline,
141
142
  }.merge(opts)
142
143
 
143
- with(grpc_rpcs: @options.grpc_rpcs.merge(
144
- rpc_name.underscore => [rpc_name, input, output, rpc_opts]
145
- ).freeze)
144
+ session_class = Class.new(self.class) do
145
+ class_eval(<<-OUT, __FILE__, __LINE__ + 1)
146
+ def #{rpc_name}(input, **opts) # def grpc_action(input, **opts)
147
+ rpc_execute("#{rpc_name}", input, **opts) # rpc_execute("grpc_action", input, **opts)
148
+ end # end
149
+ OUT
150
+ end
151
+
152
+ session_class.new(@options.merge(
153
+ grpc_rpcs: @options.grpc_rpcs.merge(
154
+ rpc_name.underscore => [rpc_name, input, output, rpc_opts]
155
+ ).freeze
156
+ ))
146
157
  end
147
158
 
148
159
  def build_stub(origin, service: nil, compression: false)
@@ -150,7 +161,32 @@ module HTTPX
150
161
 
151
162
  origin = URI.parse("#{scheme}://#{origin}")
152
163
 
153
- with(origin: origin, grpc_service: service, grpc_compression: compression)
164
+ session = self
165
+
166
+ if service && service.respond_to?(:rpc_descs)
167
+ # it's a grpc generic service
168
+ service.rpc_descs.each do |rpc_name, rpc_desc|
169
+ rpc_opts = {
170
+ marshal_method: rpc_desc.marshal_method,
171
+ unmarshal_method: rpc_desc.unmarshal_method,
172
+ }
173
+
174
+ input = rpc_desc.input
175
+ input = input.type if input.respond_to?(:type)
176
+
177
+ output = rpc_desc.output
178
+ if output.respond_to?(:type)
179
+ rpc_opts[:stream] = true
180
+ output = output.type
181
+ end
182
+
183
+ session = session.rpc(rpc_name, input, output, **rpc_opts)
184
+ end
185
+
186
+ service = service.service_name
187
+ end
188
+
189
+ session.with(origin: origin, grpc_service: service, grpc_compression: compression)
154
190
  end
155
191
 
156
192
  def execute(rpc_method, input,
@@ -166,7 +202,7 @@ module HTTPX
166
202
  private
167
203
 
168
204
  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")
205
+ rpc_name, input_enc, output_enc, rpc_opts = @options.grpc_rpcs[rpc_name]
170
206
 
171
207
  exec_opts = rpc_opts.merge(opts)
172
208
 
@@ -230,16 +266,6 @@ module HTTPX
230
266
 
231
267
  build_request(:post, uri, headers: headers, body: body)
232
268
  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
269
  end
244
270
  end
245
271
  register_plugin :grpc, GRPC
@@ -24,15 +24,19 @@ module HTTPX
24
24
  def call(connection, request, response)
25
25
  connection.upgrade_to_h2c(request, response)
26
26
  end
27
+
28
+ def extra_options(options)
29
+ options.merge(max_concurrent_requests: 1)
30
+ end
27
31
  end
28
32
 
29
33
  module InstanceMethods
30
- def send_requests(*requests, options)
34
+ def send_requests(*requests)
31
35
  upgrade_request, *remainder = requests
32
36
 
33
37
  return super unless VALID_H2C_VERBS.include?(upgrade_request.verb) && upgrade_request.scheme == "http"
34
38
 
35
- connection = pool.find_connection(upgrade_request.uri, @options.merge(options))
39
+ connection = pool.find_connection(upgrade_request.uri, upgrade_request.options)
36
40
 
37
41
  return super if connection && connection.upgrade_protocol == :h2c
38
42
 
@@ -42,7 +46,7 @@ module HTTPX
42
46
  upgrade_request.headers["upgrade"] = "h2c"
43
47
  upgrade_request.headers["http2-settings"] = HTTP2Next::Client.settings_header(upgrade_request.options.http2_settings)
44
48
 
45
- super(upgrade_request, *remainder, options.merge(max_concurrent_requests: 1))
49
+ super(upgrade_request, *remainder)
46
50
  end
47
51
  end
48
52
 
@@ -44,6 +44,11 @@ module HTTPX
44
44
  meter_elapsed_time("Session: initialized!!!")
45
45
  end
46
46
 
47
+ def close(*)
48
+ super
49
+ meter_elapsed_time("Session -> close")
50
+ end
51
+
47
52
  private
48
53
 
49
54
  def build_requests(*)
@@ -55,11 +60,6 @@ module HTTPX
55
60
  meter_elapsed_time("Session -> response") if response
56
61
  response
57
62
  end
58
-
59
- def close(*)
60
- super
61
- meter_elapsed_time("Session -> close")
62
- end
63
63
  end
64
64
 
65
65
  module RequestMethods
@@ -69,9 +69,9 @@ module HTTPX
69
69
  end
70
70
 
71
71
  def transition(nextstate)
72
- state = @state
72
+ prev_state = @state
73
73
  super
74
- meter_elapsed_time("Request##{object_id}[#{@verb} #{@uri}: #{state}] -> #{nextstate}") if nextstate == @state
74
+ meter_elapsed_time("Request##{object_id}[#{@verb} #{@uri}: #{prev_state}] -> #{@state}") if prev_state != @state
75
75
  end
76
76
  end
77
77
 
@@ -84,7 +84,7 @@ module HTTPX
84
84
  def transition(nextstate)
85
85
  state = @state
86
86
  super
87
- meter_elapsed_time("Connection[#{@origin}]: #{state} -> #{nextstate}") if nextstate == @state
87
+ meter_elapsed_time("Connection##{object_id}[#{@origin}]: #{state} -> #{nextstate}") if nextstate == @state
88
88
  end
89
89
  end
90
90
  end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+ require "delegate"
5
+
6
+ module HTTPX::Plugins
7
+ module Multipart
8
+ using HTTPX::RegexpExtensions unless Regexp.method_defined?(:match?)
9
+
10
+ CRLF = "\r\n"
11
+
12
+ class FilePart < SimpleDelegator
13
+ attr_reader :original_filename, :content_type
14
+
15
+ def initialize(filename, content_type)
16
+ @original_filename = filename
17
+ @content_type = content_type
18
+ @file = Tempfile.new("httpx", encoding: Encoding::BINARY, mode: File::RDWR)
19
+ super(@file)
20
+ end
21
+ end
22
+
23
+ TOKEN = %r{[^\s()<>,;:\\"/\[\]?=]+}.freeze
24
+ VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/.freeze
25
+ CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i.freeze
26
+ BROKEN_QUOTED = /^#{CONDISP}.*;\s*filename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i.freeze
27
+ BROKEN_UNQUOTED = /^#{CONDISP}.*;\s*filename=(#{TOKEN})/i.freeze
28
+ MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{CRLF}/ni.freeze
29
+ MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni.freeze
30
+ MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{CRLF}]*)/ni.freeze
31
+ # Updated definitions from RFC 2231
32
+ ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}.freeze
33
+ ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/.freeze
34
+ SECTION = /\*[0-9]+/.freeze
35
+ REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/.freeze
36
+ REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/.freeze
37
+ EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/.freeze
38
+ EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/.freeze
39
+ EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/.freeze
40
+ EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/.freeze
41
+ EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/.freeze
42
+ EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/.freeze
43
+ EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/.freeze
44
+ DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/.freeze
45
+ RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i.freeze
46
+
47
+ class Decoder
48
+ BOUNDARY_RE = /;\s*boundary=([^;]+)/i.freeze
49
+ WINDOW_SIZE = 2 << 14
50
+
51
+ def initialize(response)
52
+ @boundary = begin
53
+ m = response.headers["content-type"].to_s[BOUNDARY_RE, 1]
54
+ raise Error, "no boundary declared in content-type header" unless m
55
+
56
+ m.strip
57
+ end
58
+ @buffer = "".b
59
+ @parts = {}
60
+ @intermediate_boundary = "--#{@boundary}"
61
+ @state = :idle
62
+ end
63
+
64
+ def call(response, _)
65
+ response.body.each do |chunk|
66
+ @buffer << chunk
67
+
68
+ parse
69
+ end
70
+
71
+ raise Error, "invalid or unsupported multipart format" unless @buffer.empty?
72
+
73
+ @parts
74
+ end
75
+
76
+ private
77
+
78
+ def parse
79
+ case @state
80
+ when :idle
81
+ raise Error, "payload does not start with boundary" unless @buffer.start_with?("#{@intermediate_boundary}#{CRLF}")
82
+
83
+ @buffer = @buffer.byteslice(@intermediate_boundary.bytesize + 2..-1)
84
+
85
+ @state = :part_header
86
+ when :part_header
87
+ idx = @buffer.index("#{CRLF}#{CRLF}")
88
+
89
+ # raise Error, "couldn't parse part headers" unless idx
90
+ return unless idx
91
+
92
+ head = @buffer.byteslice(0..idx + 4 - 1)
93
+
94
+ @buffer = @buffer.byteslice(head.bytesize..-1)
95
+
96
+ content_type = head[MULTIPART_CONTENT_TYPE, 1]
97
+ if (name = head[MULTIPART_CONTENT_DISPOSITION, 1])
98
+ name = /\A"(.*)"\Z/ =~ name ? Regexp.last_match(1) : name.dup
99
+ name.gsub!(/\\(.)/, "\\1")
100
+ name
101
+ else
102
+ name = head[MULTIPART_CONTENT_ID, 1]
103
+ end
104
+
105
+ filename = get_filename(head)
106
+
107
+ name = filename || +"#{content_type || "text/plain"}[]" if name.nil? || name.empty?
108
+
109
+ @current = name
110
+
111
+ @parts[name] = if filename
112
+ FilePart.new(filename, content_type)
113
+ else
114
+ "".b
115
+ end
116
+
117
+ @state = :part_body
118
+ when :part_body
119
+ part = @parts[@current]
120
+
121
+ body_separator = if part.is_a?(FilePart)
122
+ "#{CRLF}#{CRLF}"
123
+ else
124
+ CRLF
125
+ end
126
+ idx = @buffer.index(body_separator)
127
+
128
+ if idx
129
+ payload = @buffer.byteslice(0..idx - 1)
130
+ @buffer = @buffer.byteslice(idx + body_separator.bytesize..-1)
131
+ part << payload
132
+ part.rewind if part.respond_to?(:rewind)
133
+ @state = :parse_boundary
134
+ else
135
+ part << @buffer
136
+ @buffer.clear
137
+ end
138
+ when :parse_boundary
139
+ raise Error, "payload does not start with boundary" unless @buffer.start_with?(@intermediate_boundary)
140
+
141
+ @buffer = @buffer.byteslice(@intermediate_boundary.bytesize..-1)
142
+
143
+ if @buffer == "--"
144
+ @buffer.clear
145
+ @state = :done
146
+ return
147
+ elsif @buffer.start_with?(CRLF)
148
+ @buffer = @buffer.byteslice(2..-1)
149
+ @state = :part_header
150
+ else
151
+ return
152
+ end
153
+ when :done
154
+ raise Error, "parsing should have been over by now"
155
+ end until @buffer.empty?
156
+ end
157
+
158
+ def get_filename(head)
159
+ filename = nil
160
+ case head
161
+ when RFC2183
162
+ params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
163
+
164
+ if (filename = params["filename"])
165
+ filename = Regexp.last_match(1) if filename =~ /^"(.*)"$/
166
+ elsif (filename = params["filename*"])
167
+ encoding, _, filename = filename.split("'", 3)
168
+ end
169
+ when BROKEN_QUOTED, BROKEN_UNQUOTED
170
+ filename = Regexp.last_match(1)
171
+ end
172
+
173
+ return unless filename
174
+
175
+ filename = URI::DEFAULT_PARSER.unescape(filename) if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
176
+
177
+ filename.scrub!
178
+
179
+ filename = filename.gsub(/\\(.)/, '\1') unless /\\[^\\"]/.match?(filename)
180
+
181
+ filename.force_encoding ::Encoding.find(encoding) if encoding
182
+
183
+ filename
184
+ end
185
+ end
186
+ end
187
+ end
@@ -17,7 +17,7 @@ module HTTPX
17
17
 
18
18
  elsif defined?(MimeMagic)
19
19
 
20
- def call(file, *)
20
+ def call(file, _)
21
21
  mime = MimeMagic.by_magic(file)
22
22
  mime.type if mime
23
23
  end
@@ -25,7 +25,7 @@ module HTTPX
25
25
  elsif system("which file", out: File::NULL)
26
26
  require "open3"
27
27
 
28
- def call(file, *)
28
+ def call(file, _)
29
29
  return if file.eof? # file command returns "application/x-empty" for empty files
30
30
 
31
31
  Open3.popen3(*%w[file --mime-type --brief -]) do |stdin, stdout, stderr, thread|
@@ -56,7 +56,7 @@ module HTTPX
56
56
 
57
57
  else
58
58
 
59
- def call(*); end
59
+ def call(_, _); end
60
60
 
61
61
  end
62
62
  end
@@ -8,7 +8,7 @@ module HTTPX
8
8
  def call(value)
9
9
  # take out specialized objects of the way
10
10
  if value.respond_to?(:filename) && value.respond_to?(:content_type) && value.respond_to?(:read)
11
- return [value, value.content_type, value.filename]
11
+ return value, value.content_type, value.filename
12
12
  end
13
13
 
14
14
  content_type = filename = nil
@@ -19,7 +19,7 @@ module HTTPX
19
19
  value = value[:body]
20
20
  end
21
21
 
22
- value = value.open(:binmode => true) if Object.const_defined?(:Pathname) && value.is_a?(Pathname)
22
+ value = value.open(File::RDONLY) 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)