httpx 0.16.0 → 0.18.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/doc/release_notes/0_16_1.md +5 -0
  3. data/doc/release_notes/0_17_0.md +49 -0
  4. data/doc/release_notes/0_18_0.md +69 -0
  5. data/doc/release_notes/0_18_1.md +12 -0
  6. data/lib/httpx/adapters/datadog.rb +1 -1
  7. data/lib/httpx/adapters/faraday.rb +5 -3
  8. data/lib/httpx/adapters/webmock.rb +9 -3
  9. data/lib/httpx/altsvc.rb +2 -2
  10. data/lib/httpx/chainable.rb +4 -4
  11. data/lib/httpx/connection/http1.rb +23 -14
  12. data/lib/httpx/connection/http2.rb +35 -17
  13. data/lib/httpx/connection.rb +74 -76
  14. data/lib/httpx/domain_name.rb +1 -1
  15. data/lib/httpx/extensions.rb +50 -4
  16. data/lib/httpx/headers.rb +1 -1
  17. data/lib/httpx/io/ssl.rb +5 -1
  18. data/lib/httpx/io/tls.rb +7 -7
  19. data/lib/httpx/loggable.rb +5 -5
  20. data/lib/httpx/options.rb +35 -13
  21. data/lib/httpx/parser/http1.rb +10 -6
  22. data/lib/httpx/plugins/aws_sdk_authentication.rb +42 -18
  23. data/lib/httpx/plugins/aws_sigv4.rb +9 -11
  24. data/lib/httpx/plugins/compression.rb +5 -3
  25. data/lib/httpx/plugins/cookies/jar.rb +1 -1
  26. data/lib/httpx/plugins/digest_authentication.rb +4 -4
  27. data/lib/httpx/plugins/expect.rb +7 -3
  28. data/lib/httpx/plugins/grpc/message.rb +2 -2
  29. data/lib/httpx/plugins/grpc.rb +3 -3
  30. data/lib/httpx/plugins/h2c.rb +7 -3
  31. data/lib/httpx/plugins/internal_telemetry.rb +8 -8
  32. data/lib/httpx/plugins/multipart/decoder.rb +187 -0
  33. data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
  34. data/lib/httpx/plugins/multipart/part.rb +2 -2
  35. data/lib/httpx/plugins/multipart.rb +16 -2
  36. data/lib/httpx/plugins/ntlm_authentication.rb +4 -4
  37. data/lib/httpx/plugins/proxy/ssh.rb +11 -4
  38. data/lib/httpx/plugins/proxy.rb +6 -4
  39. data/lib/httpx/plugins/response_cache/store.rb +55 -0
  40. data/lib/httpx/plugins/response_cache.rb +88 -0
  41. data/lib/httpx/plugins/retries.rb +36 -14
  42. data/lib/httpx/plugins/stream.rb +3 -4
  43. data/lib/httpx/pool.rb +39 -13
  44. data/lib/httpx/registry.rb +1 -1
  45. data/lib/httpx/request.rb +12 -13
  46. data/lib/httpx/resolver/https.rb +5 -7
  47. data/lib/httpx/resolver/native.rb +19 -5
  48. data/lib/httpx/resolver/resolver_mixin.rb +2 -1
  49. data/lib/httpx/resolver/system.rb +2 -0
  50. data/lib/httpx/resolver.rb +2 -2
  51. data/lib/httpx/response.rb +60 -44
  52. data/lib/httpx/selector.rb +9 -19
  53. data/lib/httpx/session.rb +22 -15
  54. data/lib/httpx/session2.rb +3 -1
  55. data/lib/httpx/timers.rb +84 -0
  56. data/lib/httpx/transcoder/body.rb +2 -1
  57. data/lib/httpx/transcoder/form.rb +20 -0
  58. data/lib/httpx/transcoder/json.rb +12 -0
  59. data/lib/httpx/transcoder.rb +62 -1
  60. data/lib/httpx/utils.rb +10 -2
  61. data/lib/httpx/version.rb +1 -1
  62. data/lib/httpx.rb +1 -0
  63. data/sig/buffer.rbs +2 -2
  64. data/sig/chainable.rbs +7 -1
  65. data/sig/connection/http1.rbs +15 -4
  66. data/sig/connection/http2.rbs +19 -5
  67. data/sig/connection.rbs +15 -9
  68. data/sig/headers.rbs +19 -18
  69. data/sig/options.rbs +13 -5
  70. data/sig/parser/http1.rbs +3 -3
  71. data/sig/plugins/aws_sdk_authentication.rbs +22 -4
  72. data/sig/plugins/aws_sigv4.rbs +12 -3
  73. data/sig/plugins/basic_authentication.rbs +1 -1
  74. data/sig/plugins/multipart.rbs +64 -8
  75. data/sig/plugins/proxy.rbs +6 -6
  76. data/sig/plugins/response_cache.rbs +35 -0
  77. data/sig/plugins/retries.rbs +3 -0
  78. data/sig/pool.rbs +6 -0
  79. data/sig/request.rbs +11 -8
  80. data/sig/resolver/native.rbs +2 -1
  81. data/sig/resolver/resolver_mixin.rbs +1 -1
  82. data/sig/resolver/system.rbs +3 -1
  83. data/sig/response.rbs +11 -4
  84. data/sig/selector.rbs +8 -6
  85. data/sig/session.rbs +8 -14
  86. data/sig/timers.rbs +32 -0
  87. data/sig/transcoder/form.rbs +1 -0
  88. data/sig/transcoder/json.rbs +1 -0
  89. data/sig/transcoder.rbs +5 -4
  90. data/sig/utils.rbs +4 -0
  91. metadata +62 -61
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
- require "aws-sdk-s3"
5
-
6
3
  module HTTPX
7
4
  module Plugins
8
5
  #
@@ -75,16 +72,16 @@ module HTTPX
75
72
 
76
73
  # canonical request
77
74
  creq = "#{request.verb.to_s.upcase}" \
78
- "\n#{request.canonical_path}" \
79
- "\n#{request.canonical_query}" \
80
- "\n#{canonical_headers}" \
81
- "\n#{signed_headers}" \
82
- "\n#{content_hashed}"
75
+ "\n#{request.canonical_path}" \
76
+ "\n#{request.canonical_query}" \
77
+ "\n#{canonical_headers}" \
78
+ "\n#{signed_headers}" \
79
+ "\n#{content_hashed}"
83
80
 
84
81
  credential_scope = "#{date}" \
85
- "/#{@region}" \
86
- "/#{@service}" \
87
- "/#{lower_provider_prefix}_request"
82
+ "/#{@region}" \
83
+ "/#{@service}" \
84
+ "/#{lower_provider_prefix}_request"
88
85
 
89
86
  algo_line = "#{upper_provider_prefix}-HMAC-#{@algorithm}"
90
87
  # string to sign
@@ -142,6 +139,7 @@ module HTTPX
142
139
 
143
140
  class << self
144
141
  def load_dependencies(*)
142
+ require "set"
145
143
  require "digest/sha2"
146
144
  require "openssl"
147
145
  end
@@ -72,6 +72,8 @@ module HTTPX
72
72
  end
73
73
 
74
74
  module ResponseBodyMethods
75
+ using ArrayExtensions
76
+
75
77
  attr_reader :encodings
76
78
 
77
79
  def initialize(*)
@@ -90,7 +92,7 @@ module HTTPX
90
92
  Float::INFINITY
91
93
  end
92
94
 
93
- @_inflaters = @headers.get("content-encoding").map do |encoding|
95
+ @_inflaters = @headers.get("content-encoding").filter_map do |encoding|
94
96
  next if encoding == "identity"
95
97
 
96
98
  inflater = @options.encodings.registry(encoding).inflater(compressed_length)
@@ -100,7 +102,7 @@ module HTTPX
100
102
 
101
103
  @encodings << encoding
102
104
  inflater
103
- end.compact
105
+ end
104
106
 
105
107
  # this can happen if the only declared encoding is "identity"
106
108
  remove_instance_variable(:@_inflaters) if @_inflaters.empty?
@@ -134,7 +136,7 @@ module HTTPX
134
136
  end
135
137
 
136
138
  def each(&blk)
137
- return enum_for(__method__) unless block_given?
139
+ return enum_for(__method__) unless blk
138
140
 
139
141
  return deflate(&blk) if @buffer.size.zero?
140
142
 
@@ -55,7 +55,7 @@ module HTTPX
55
55
  end
56
56
 
57
57
  def each(uri = nil, &blk)
58
- return enum_for(__method__, uri) unless block_given?
58
+ return enum_for(__method__, uri) unless blk
59
59
 
60
60
  return @cookies.each(&blk) unless uri
61
61
 
@@ -40,12 +40,12 @@ module HTTPX
40
40
 
41
41
  alias_method :digest_auth, :digest_authentication
42
42
 
43
- def send_requests(*requests, options)
43
+ def send_requests(*requests)
44
44
  requests.flat_map do |request|
45
45
  digest = request.options.digest
46
46
 
47
47
  if digest
48
- probe_response = wrap { super(request, options).first }
48
+ probe_response = wrap { super(request).first }
49
49
 
50
50
  if digest && !probe_response.is_a?(ErrorResponse) &&
51
51
  probe_response.status == 401 && probe_response.headers.key?("www-authenticate") &&
@@ -56,12 +56,12 @@ module HTTPX
56
56
  token = digest.generate_header(request, probe_response)
57
57
  request.headers["authorization"] = "Digest #{token}"
58
58
 
59
- super(request, options)
59
+ super(request)
60
60
  else
61
61
  probe_response
62
62
  end
63
63
  else
64
- super(request, options)
64
+ super(request)
65
65
  end
66
66
  end
67
67
  end
@@ -69,9 +69,14 @@ module HTTPX
69
69
  end
70
70
 
71
71
  module ConnectionMethods
72
- def send(request)
72
+ def send_request_to_parser(request)
73
+ super
74
+
75
+ return unless request.headers["expect"] == "100-continue"
76
+
73
77
  request.once(:expect) do
74
- @timers.after(@options.expect_timeout) do
78
+ @timers.after(request.options.expect_timeout) do
79
+ # expect timeout expired
75
80
  if request.state == :expect && !request.expects?
76
81
  Expect.no_expect_store << request.origin
77
82
  request.headers.delete("expect")
@@ -79,7 +84,6 @@ module HTTPX
79
84
  end
80
85
  end
81
86
  end
82
- super
83
87
  end
84
88
  end
85
89
 
@@ -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
 
@@ -143,9 +143,9 @@ module HTTPX
143
143
 
144
144
  session_class = Class.new(self.class) do
145
145
  class_eval(<<-OUT, __FILE__, __LINE__ + 1)
146
- def #{rpc_name}(input, **opts)
147
- rpc_execute("#{rpc_name}", input, **opts)
148
- end
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
149
  OUT
150
150
  end
151
151
 
@@ -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)
@@ -29,13 +29,14 @@ module HTTPX
29
29
  # in order not to break legacy code, we'll keep loading http/form_data for them.
30
30
  require "http/form_data"
31
31
  warn "httpx: http/form_data is no longer a requirement to use HTTPX :multipart plugin. See migration instructions under" \
32
- "https://honeyryderchuck.gitlab.io/httpx/wiki/Multipart-Uploads.html#notes. \n\n" \
33
- "If you'd like to stop seeing this message, require 'http/form_data' yourself."
32
+ "https://honeyryderchuck.gitlab.io/httpx/wiki/Multipart-Uploads.html#notes. \n\n" \
33
+ "If you'd like to stop seeing this message, require 'http/form_data' yourself."
34
34
  end
35
35
  rescue LoadError
36
36
  end
37
37
  # :nocov:
38
38
  require "httpx/plugins/multipart/encoder"
39
+ require "httpx/plugins/multipart/decoder"
39
40
  require "httpx/plugins/multipart/part"
40
41
  require "httpx/plugins/multipart/mime_type_detector"
41
42
  end
@@ -56,6 +57,19 @@ module HTTPX
56
57
  end
57
58
  end
58
59
 
60
+ def decode(response)
61
+ content_type = response.content_type.mime_type
62
+
63
+ case content_type
64
+ when "application/x-www-form-urlencoded"
65
+ Transcoder::Form.decode(response)
66
+ when "multipart/form-data"
67
+ Decoder.new(response)
68
+ else
69
+ raise Error, "invalid form mime type (#{content_type})"
70
+ end
71
+ end
72
+
59
73
  def multipart?(data)
60
74
  data.any? do |_, v|
61
75
  MULTIPART_VALUE_COND.call(v) ||
@@ -34,13 +34,13 @@ module HTTPX
34
34
 
35
35
  alias_method :ntlm_auth, :ntlm_authentication
36
36
 
37
- def send_requests(*requests, options)
37
+ def send_requests(*requests)
38
38
  requests.flat_map do |request|
39
39
  ntlm = request.options.ntlm
40
40
 
41
41
  if ntlm
42
42
  request.headers["authorization"] = "NTLM #{NTLM.negotiate(domain: ntlm.domain).to_base64}"
43
- probe_response = wrap { super(request, options).first }
43
+ probe_response = wrap { super(request).first }
44
44
 
45
45
  if !probe_response.is_a?(ErrorResponse) && probe_response.status == 401 &&
46
46
  probe_response.headers.key?("www-authenticate") &&
@@ -52,12 +52,12 @@ module HTTPX
52
52
  request.transition(:idle)
53
53
 
54
54
  request.headers["authorization"] = "NTLM #{ntlm_challenge}"
55
- super(request, options)
55
+ super(request)
56
56
  else
57
57
  probe_response
58
58
  end
59
59
  else
60
- super(request, options)
60
+ super(request)
61
61
  end
62
62
  end
63
63
  end
@@ -19,10 +19,14 @@ module HTTPX
19
19
  end
20
20
 
21
21
  module InstanceMethods
22
- private
22
+ def request(*args, **options)
23
+ raise ArgumentError, "must perform at least one request" if args.empty?
24
+
25
+ requests = args.first.is_a?(Request) ? args : build_requests(*args, options)
26
+
27
+ request = requests.first or return super
23
28
 
24
- def send_requests(*requests, options)
25
- request_options = @options.merge(options)
29
+ request_options = request.options
26
30
 
27
31
  return super unless request_options.proxy
28
32
 
@@ -37,18 +41,21 @@ module HTTPX
37
41
  if request_options.debug
38
42
  ssh_options[:verbose] = request_options.debug_level == 2 ? :debug : :info
39
43
  end
44
+
40
45
  request_uri = URI(requests.first.uri)
41
46
  @_gateway = Net::SSH::Gateway.new(ssh_uri.host, ssh_username, ssh_options)
42
47
  begin
43
48
  @_gateway.open(request_uri.host, request_uri.port) do |local_port|
44
49
  io = build_gateway_socket(local_port, request_uri, request_options)
45
- super(*requests, options.merge(io: io))
50
+ super(*args, **options.merge(io: io))
46
51
  end
47
52
  ensure
48
53
  @_gateway.shutdown!
49
54
  end
50
55
  end
51
56
 
57
+ private
58
+
52
59
  def build_gateway_socket(port, request_uri, options)
53
60
  case request_uri.scheme
54
61
  when "https"
@@ -5,7 +5,8 @@ require "ipaddr"
5
5
  require "forwardable"
6
6
 
7
7
  module HTTPX
8
- HTTPProxyError = Class.new(Error)
8
+ class HTTPProxyError < Error; end
9
+
9
10
  module Plugins
10
11
  #
11
12
  # This plugin adds support for proxies. It ships with support for:
@@ -136,10 +137,11 @@ module HTTPX
136
137
  def __proxy_error?(response)
137
138
  error = response.error
138
139
  case error
139
- when ResolveError
140
+ when NativeResolveError
140
141
  # failed resolving proxy domain
141
- proxy_uri = error.connection.options.proxy.uri
142
- proxy_uri.to_s == @_proxy_uris.first
142
+ error.connection.origin.to_s == @_proxy_uris.first
143
+ when ResolveError
144
+ error.message.end_with?(@_proxy_uris.first)
143
145
  when *PROXY_ERRORS
144
146
  # timeout errors connecting to proxy
145
147
  true
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module HTTPX::Plugins
6
+ module ResponseCache
7
+ class Store
8
+ extend Forwardable
9
+
10
+ def_delegator :@store, :clear
11
+
12
+ def initialize
13
+ @store = {}
14
+ end
15
+
16
+ def lookup(uri)
17
+ @store[uri]
18
+ end
19
+
20
+ def cached?(uri)
21
+ @store.key?(uri)
22
+ end
23
+
24
+ def cache(uri, response)
25
+ @store[uri] = response
26
+ end
27
+
28
+ def prepare(request)
29
+ cached_response = @store[request.uri]
30
+
31
+ return unless cached_response
32
+
33
+ original_request = cached_response.instance_variable_get(:@request)
34
+
35
+ if (vary = cached_response.headers["vary"])
36
+ if vary == "*"
37
+ return unless request.headers.same_headers?(original_request.headers)
38
+ else
39
+ return unless vary.split(/ *, */).all? do |cache_field|
40
+ !original_request.headers.key?(cache_field) || request.headers[cache_field] == original_request.headers[cache_field]
41
+ end
42
+ end
43
+ end
44
+
45
+ if !request.headers.key?("if-modified-since") && (last_modified = cached_response.headers["last-modified"])
46
+ request.headers.add("if-modified-since", last_modified)
47
+ end
48
+
49
+ if !request.headers.key?("if-none-match") && (etag = cached_response.headers["etag"]) # rubocop:disable Style/GuardClause
50
+ request.headers.add("if-none-match", etag)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end