httpx 0.16.1 → 0.18.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -3
  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/doc/release_notes/0_18_2.md +10 -0
  7. data/lib/httpx/adapters/datadog.rb +1 -1
  8. data/lib/httpx/adapters/faraday.rb +5 -3
  9. data/lib/httpx/adapters/webmock.rb +9 -3
  10. data/lib/httpx/altsvc.rb +2 -2
  11. data/lib/httpx/chainable.rb +4 -4
  12. data/lib/httpx/connection/http1.rb +23 -14
  13. data/lib/httpx/connection/http2.rb +35 -17
  14. data/lib/httpx/connection.rb +74 -76
  15. data/lib/httpx/domain_name.rb +1 -1
  16. data/lib/httpx/extensions.rb +50 -4
  17. data/lib/httpx/headers.rb +1 -1
  18. data/lib/httpx/io/ssl.rb +5 -1
  19. data/lib/httpx/io/tls.rb +7 -7
  20. data/lib/httpx/loggable.rb +5 -5
  21. data/lib/httpx/options.rb +35 -13
  22. data/lib/httpx/parser/http1.rb +10 -6
  23. data/lib/httpx/plugins/aws_sdk_authentication.rb +42 -18
  24. data/lib/httpx/plugins/aws_sigv4.rb +9 -11
  25. data/lib/httpx/plugins/compression.rb +5 -3
  26. data/lib/httpx/plugins/cookies/jar.rb +1 -1
  27. data/lib/httpx/plugins/digest_authentication.rb +4 -4
  28. data/lib/httpx/plugins/expect.rb +7 -3
  29. data/lib/httpx/plugins/grpc/message.rb +2 -2
  30. data/lib/httpx/plugins/grpc.rb +3 -3
  31. data/lib/httpx/plugins/h2c.rb +7 -3
  32. data/lib/httpx/plugins/internal_telemetry.rb +8 -8
  33. data/lib/httpx/plugins/multipart/decoder.rb +187 -0
  34. data/lib/httpx/plugins/multipart/mime_type_detector.rb +3 -3
  35. data/lib/httpx/plugins/multipart/part.rb +2 -2
  36. data/lib/httpx/plugins/multipart.rb +16 -2
  37. data/lib/httpx/plugins/ntlm_authentication.rb +4 -4
  38. data/lib/httpx/plugins/proxy/ssh.rb +11 -4
  39. data/lib/httpx/plugins/proxy.rb +6 -4
  40. data/lib/httpx/plugins/response_cache/store.rb +55 -0
  41. data/lib/httpx/plugins/response_cache.rb +88 -0
  42. data/lib/httpx/plugins/retries.rb +36 -14
  43. data/lib/httpx/plugins/stream.rb +3 -4
  44. data/lib/httpx/pool.rb +39 -13
  45. data/lib/httpx/registry.rb +1 -1
  46. data/lib/httpx/request.rb +12 -13
  47. data/lib/httpx/resolver/https.rb +5 -7
  48. data/lib/httpx/resolver/native.rb +4 -2
  49. data/lib/httpx/resolver/resolver_mixin.rb +2 -1
  50. data/lib/httpx/resolver/system.rb +2 -0
  51. data/lib/httpx/resolver.rb +2 -2
  52. data/lib/httpx/response.rb +60 -44
  53. data/lib/httpx/selector.rb +16 -19
  54. data/lib/httpx/session.rb +22 -15
  55. data/lib/httpx/session2.rb +1 -1
  56. data/lib/httpx/timers.rb +84 -0
  57. data/lib/httpx/transcoder/body.rb +2 -1
  58. data/lib/httpx/transcoder/form.rb +20 -0
  59. data/lib/httpx/transcoder/json.rb +12 -0
  60. data/lib/httpx/transcoder.rb +62 -1
  61. data/lib/httpx/utils.rb +10 -2
  62. data/lib/httpx/version.rb +1 -1
  63. data/lib/httpx.rb +1 -0
  64. data/sig/buffer.rbs +2 -2
  65. data/sig/chainable.rbs +7 -1
  66. data/sig/connection/http1.rbs +15 -4
  67. data/sig/connection/http2.rbs +19 -5
  68. data/sig/connection.rbs +15 -9
  69. data/sig/headers.rbs +19 -18
  70. data/sig/options.rbs +13 -5
  71. data/sig/parser/http1.rbs +3 -3
  72. data/sig/plugins/aws_sdk_authentication.rbs +22 -4
  73. data/sig/plugins/aws_sigv4.rbs +12 -3
  74. data/sig/plugins/basic_authentication.rbs +1 -1
  75. data/sig/plugins/multipart.rbs +64 -8
  76. data/sig/plugins/proxy.rbs +6 -6
  77. data/sig/plugins/response_cache.rbs +35 -0
  78. data/sig/plugins/retries.rbs +3 -0
  79. data/sig/pool.rbs +6 -0
  80. data/sig/request.rbs +11 -8
  81. data/sig/resolver/native.rbs +2 -1
  82. data/sig/resolver/resolver_mixin.rbs +1 -1
  83. data/sig/resolver/system.rbs +3 -1
  84. data/sig/response.rbs +11 -4
  85. data/sig/selector.rbs +8 -6
  86. data/sig/session.rbs +8 -14
  87. data/sig/timers.rbs +32 -0
  88. data/sig/transcoder/form.rbs +1 -0
  89. data/sig/transcoder/json.rbs +1 -0
  90. data/sig/transcoder.rbs +5 -4
  91. data/sig/utils.rbs +4 -0
  92. metadata +18 -17
@@ -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