http 5.1.1 → 5.3.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +6 -24
  3. data/.rubocop/metrics.yml +4 -0
  4. data/.rubocop/rspec.yml +9 -0
  5. data/.rubocop.yml +1 -0
  6. data/.rubocop_todo.yml +45 -32
  7. data/CHANGELOG.md +57 -0
  8. data/{CHANGES.md → CHANGES_OLD.md} +1 -1
  9. data/Gemfile +1 -0
  10. data/README.md +4 -2
  11. data/SECURITY.md +13 -1
  12. data/http.gemspec +8 -2
  13. data/lib/http/base64.rb +12 -0
  14. data/lib/http/chainable.rb +27 -3
  15. data/lib/http/client.rb +1 -1
  16. data/lib/http/connection.rb +12 -3
  17. data/lib/http/errors.rb +16 -0
  18. data/lib/http/feature.rb +2 -1
  19. data/lib/http/features/instrumentation.rb +6 -1
  20. data/lib/http/features/raise_error.rb +22 -0
  21. data/lib/http/headers/normalizer.rb +69 -0
  22. data/lib/http/headers.rb +26 -40
  23. data/lib/http/request/writer.rb +2 -1
  24. data/lib/http/request.rb +15 -5
  25. data/lib/http/retriable/client.rb +37 -0
  26. data/lib/http/retriable/delay_calculator.rb +64 -0
  27. data/lib/http/retriable/errors.rb +14 -0
  28. data/lib/http/retriable/performer.rb +153 -0
  29. data/lib/http/timeout/null.rb +8 -5
  30. data/lib/http/uri.rb +18 -2
  31. data/lib/http/version.rb +1 -1
  32. data/lib/http.rb +1 -0
  33. data/spec/lib/http/client_spec.rb +1 -0
  34. data/spec/lib/http/connection_spec.rb +23 -1
  35. data/spec/lib/http/features/instrumentation_spec.rb +19 -0
  36. data/spec/lib/http/features/raise_error_spec.rb +62 -0
  37. data/spec/lib/http/headers/normalizer_spec.rb +52 -0
  38. data/spec/lib/http/options/headers_spec.rb +5 -1
  39. data/spec/lib/http/redirector_spec.rb +6 -5
  40. data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
  41. data/spec/lib/http/retriable/performer_spec.rb +302 -0
  42. data/spec/lib/http/uri/normalizer_spec.rb +95 -0
  43. data/spec/lib/http_spec.rb +49 -3
  44. data/spec/spec_helper.rb +1 -0
  45. data/spec/support/dummy_server/servlet.rb +19 -6
  46. data/spec/support/dummy_server.rb +2 -1
  47. metadata +28 -8
data/lib/http/headers.rb CHANGED
@@ -4,6 +4,7 @@ require "forwardable"
4
4
 
5
5
  require "http/errors"
6
6
  require "http/headers/mixin"
7
+ require "http/headers/normalizer"
7
8
  require "http/headers/known"
8
9
 
9
10
  module HTTP
@@ -12,12 +13,31 @@ module HTTP
12
13
  extend Forwardable
13
14
  include Enumerable
14
15
 
15
- # Matches HTTP header names when in "Canonical-Http-Format"
16
- CANONICAL_NAME_RE = /\A[A-Z][a-z]*(?:-[A-Z][a-z]*)*\z/.freeze
16
+ class << self
17
+ # Coerces given `object` into Headers.
18
+ #
19
+ # @raise [Error] if object can't be coerced
20
+ # @param [#to_hash, #to_h, #to_a] object
21
+ # @return [Headers]
22
+ def coerce(object)
23
+ unless object.is_a? self
24
+ object = case
25
+ when object.respond_to?(:to_hash) then object.to_hash
26
+ when object.respond_to?(:to_h) then object.to_h
27
+ when object.respond_to?(:to_a) then object.to_a
28
+ else raise Error, "Can't coerce #{object.inspect} to Headers"
29
+ end
30
+ end
31
+ headers = new
32
+ object.each { |k, v| headers.add k, v }
33
+ headers
34
+ end
35
+ alias [] coerce
17
36
 
18
- # Matches valid header field name according to RFC.
19
- # @see http://tools.ietf.org/html/rfc7230#section-3.2
20
- COMPLIANT_NAME_RE = /\A[A-Za-z0-9!#$%&'*+\-.^_`|~]+\z/.freeze
37
+ def normalizer
38
+ @normalizer ||= Headers::Normalizer.new
39
+ end
40
+ end
21
41
 
22
42
  # Class constructor.
23
43
  def initialize
@@ -194,45 +214,11 @@ module HTTP
194
214
  dup.tap { |dupped| dupped.merge! other }
195
215
  end
196
216
 
197
- class << self
198
- # Coerces given `object` into Headers.
199
- #
200
- # @raise [Error] if object can't be coerced
201
- # @param [#to_hash, #to_h, #to_a] object
202
- # @return [Headers]
203
- def coerce(object)
204
- unless object.is_a? self
205
- object = case
206
- when object.respond_to?(:to_hash) then object.to_hash
207
- when object.respond_to?(:to_h) then object.to_h
208
- when object.respond_to?(:to_a) then object.to_a
209
- else raise Error, "Can't coerce #{object.inspect} to Headers"
210
- end
211
- end
212
-
213
- headers = new
214
- object.each { |k, v| headers.add k, v }
215
- headers
216
- end
217
- alias [] coerce
218
- end
219
-
220
217
  private
221
218
 
222
219
  # Transforms `name` to canonical HTTP header capitalization
223
- #
224
- # @param [String] name
225
- # @raise [HeaderError] if normalized name does not
226
- # match {HEADER_NAME_RE}
227
- # @return [String] canonical HTTP header name
228
220
  def normalize_header(name)
229
- return name if name =~ CANONICAL_NAME_RE
230
-
231
- normalized = name.split(/[\-_]/).each(&:capitalize!).join("-")
232
-
233
- return normalized if normalized =~ COMPLIANT_NAME_RE
234
-
235
- raise HeaderError, "Invalid HTTP header field name: #{name.inspect}"
221
+ self.class.normalizer.call(name)
236
222
  end
237
223
 
238
224
  # Ensures there is no new line character in the header value
@@ -108,6 +108,7 @@ module HTTP
108
108
 
109
109
  private
110
110
 
111
+ # @raise [SocketWriteError] when unable to write to socket
111
112
  def write(data)
112
113
  until data.empty?
113
114
  length = @socket.write(data)
@@ -118,7 +119,7 @@ module HTTP
118
119
  rescue Errno::EPIPE
119
120
  raise
120
121
  rescue IOError, SocketError, SystemCallError => e
121
- raise ConnectionError, "error writing to socket: #{e}", e.backtrace
122
+ raise SocketWriteError, "error writing to socket: #{e}", e.backtrace
122
123
  end
123
124
  end
124
125
  end
data/lib/http/request.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "forwardable"
4
- require "base64"
5
4
  require "time"
6
5
 
6
+ require "http/base64"
7
7
  require "http/errors"
8
8
  require "http/headers"
9
9
  require "http/request/body"
@@ -15,6 +15,7 @@ module HTTP
15
15
  class Request
16
16
  extend Forwardable
17
17
 
18
+ include HTTP::Base64
18
19
  include HTTP::Headers::Mixin
19
20
 
20
21
  # The method given was not understood
@@ -49,7 +50,10 @@ module HTTP
49
50
  :search,
50
51
 
51
52
  # RFC 4791: Calendaring Extensions to WebDAV -- CalDAV
52
- :mkcalendar
53
+ :mkcalendar,
54
+
55
+ # Implemented by several caching servers, like Squid, Varnish or Fastly
56
+ :purge
53
57
  ].freeze
54
58
 
55
59
  # Allowed schemes
@@ -156,7 +160,7 @@ module HTTP
156
160
  end
157
161
 
158
162
  def proxy_authorization_header
159
- digest = Base64.strict_encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}")
163
+ digest = encode64("#{proxy[:proxy_username]}:#{proxy[:proxy_password]}")
160
164
  "Basic #{digest}"
161
165
  end
162
166
 
@@ -172,7 +176,9 @@ module HTTP
172
176
  uri.omit(:fragment)
173
177
  else
174
178
  uri.request_uri
175
- end
179
+ end.to_s
180
+
181
+ raise RequestError, "Invalid request URI: #{request_uri.inspect}" if request_uri.match?(/\s/)
176
182
 
177
183
  "#{verb.to_s.upcase} #{request_uri} HTTP/#{version}"
178
184
  end
@@ -230,7 +236,11 @@ module HTTP
230
236
 
231
237
  # @return [String] Default host (with port if needed) header value.
232
238
  def default_host_header_value
233
- PORTS[@scheme] == port ? host : "#{host}:#{port}"
239
+ value = PORTS[@scheme] == port ? host : "#{host}:#{port}"
240
+
241
+ raise RequestError, "Invalid host: #{value.inspect}" if value.match?(/\s/)
242
+
243
+ value
234
244
  end
235
245
 
236
246
  def prepare_body(body)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "http/retriable/performer"
4
+
5
+ module HTTP
6
+ module Retriable
7
+ # Retriable version of HTTP::Client.
8
+ #
9
+ # @see http://www.rubydoc.info/gems/http/HTTP/Client
10
+ class Client < HTTP::Client
11
+ # @param [Performer] performer
12
+ # @param [HTTP::Options, Hash] options
13
+ def initialize(performer, options)
14
+ @performer = performer
15
+ super(options)
16
+ end
17
+
18
+ # Overriden version of `HTTP::Client#make_request`.
19
+ #
20
+ # Monitors request/response phase with performer.
21
+ #
22
+ # @see http://www.rubydoc.info/gems/http/HTTP/Client:perform
23
+ def perform(req, options)
24
+ @performer.perform(self, req) { super(req, options) }
25
+ end
26
+
27
+ private
28
+
29
+ # Overriden version of `HTTP::Chainable#branch`.
30
+ #
31
+ # @return [HTTP::Retriable::Client]
32
+ def branch(options)
33
+ Retriable::Client.new(@performer, options)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ module Retriable
5
+ # @api private
6
+ class DelayCalculator
7
+ def initialize(opts)
8
+ @max_delay = opts.fetch(:max_delay, Float::MAX).to_f
9
+ if (delay = opts[:delay]).respond_to?(:call)
10
+ @delay_proc = opts.fetch(:delay)
11
+ else
12
+ @delay = delay
13
+ end
14
+ end
15
+
16
+ def call(iteration, response)
17
+ delay = if response && (retry_header = response.headers["Retry-After"])
18
+ delay_from_retry_header(retry_header)
19
+ else
20
+ calculate_delay_from_iteration(iteration)
21
+ end
22
+
23
+ ensure_dealy_in_bounds(delay)
24
+ end
25
+
26
+ RFC2822_DATE_REGEX = /^
27
+ (?:Sun|Mon|Tue|Wed|Thu|Fri|Sat),\s+
28
+ (?:0[1-9]|[1-2]?[0-9]|3[01])\s+
29
+ (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+
30
+ (?:19[0-9]{2}|[2-9][0-9]{3})\s+
31
+ (?:2[0-3]|[0-1][0-9]):(?:[0-5][0-9]):(?:60|[0-5][0-9])\s+
32
+ GMT
33
+ $/x
34
+
35
+ # Spec for Retry-After header
36
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
37
+ def delay_from_retry_header(value)
38
+ value = value.to_s.strip
39
+
40
+ case value
41
+ when RFC2822_DATE_REGEX then DateTime.rfc2822(value).to_time - Time.now.utc
42
+ when /^\d+$/ then value.to_i
43
+ else 0
44
+ end
45
+ end
46
+
47
+ def calculate_delay_from_iteration(iteration)
48
+ if @delay_proc
49
+ @delay_proc.call(iteration)
50
+ elsif @delay
51
+ @delay
52
+ else
53
+ delay = (2**(iteration - 1)) - 1
54
+ delay_noise = rand
55
+ delay + delay_noise
56
+ end
57
+ end
58
+
59
+ def ensure_dealy_in_bounds(delay)
60
+ delay.clamp(0, @max_delay)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HTTP
4
+ # Retriable performance ran out of attempts
5
+ class OutOfRetriesError < Error
6
+ attr_accessor :response
7
+
8
+ attr_writer :cause
9
+
10
+ def cause
11
+ @cause || super
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "http/retriable/errors"
5
+ require "http/retriable/delay_calculator"
6
+ require "openssl"
7
+
8
+ module HTTP
9
+ module Retriable
10
+ # Request performing watchdog.
11
+ # @api private
12
+ class Performer
13
+ # Exceptions we should retry
14
+ RETRIABLE_ERRORS = [
15
+ HTTP::TimeoutError,
16
+ HTTP::ConnectionError,
17
+ IO::EAGAINWaitReadable,
18
+ Errno::ECONNRESET,
19
+ Errno::ECONNREFUSED,
20
+ Errno::EHOSTUNREACH,
21
+ OpenSSL::SSL::SSLError,
22
+ EOFError,
23
+ IOError
24
+ ].freeze
25
+
26
+ # @param [Hash] opts
27
+ # @option opts [#to_i] :tries (5)
28
+ # @option opts [#call, #to_i] :delay (DELAY_PROC)
29
+ # @option opts [Array(Exception)] :exceptions (RETRIABLE_ERRORS)
30
+ # @option opts [Array(#to_i)] :retry_statuses
31
+ # @option opts [#call] :on_retry
32
+ # @option opts [#to_f] :max_delay (Float::MAX)
33
+ # @option opts [#call] :should_retry
34
+ def initialize(opts)
35
+ @exception_classes = opts.fetch(:exceptions, RETRIABLE_ERRORS)
36
+ @retry_statuses = opts[:retry_statuses]
37
+ @tries = opts.fetch(:tries, 5).to_i
38
+ @on_retry = opts.fetch(:on_retry, ->(*) {})
39
+ @should_retry_proc = opts[:should_retry]
40
+ @delay_calculator = DelayCalculator.new(opts)
41
+ end
42
+
43
+ # Watches request/response execution.
44
+ #
45
+ # If any of {RETRIABLE_ERRORS} occur or response status is `5xx`, retries
46
+ # up to `:tries` amount of times. Sleeps for amount of seconds calculated
47
+ # with `:delay` proc before each retry.
48
+ #
49
+ # @see #initialize
50
+ # @api private
51
+ def perform(client, req, &block)
52
+ 1.upto(Float::INFINITY) do |attempt| # infinite loop with index
53
+ err, res = try_request(&block)
54
+
55
+ if retry_request?(req, err, res, attempt)
56
+ begin
57
+ wait_for_retry_or_raise(req, err, res, attempt)
58
+ ensure
59
+ # Some servers support Keep-Alive on any response. Thus we should
60
+ # flush response before retry, to avoid state error (when socket
61
+ # has pending response data and we try to write new request).
62
+ # Alternatively, as we don't need response body here at all, we
63
+ # are going to close client, effectivle closing underlying socket
64
+ # and resetting client's state.
65
+ client.close
66
+ end
67
+ elsif err
68
+ client.close
69
+ raise err
70
+ elsif res
71
+ return res
72
+ end
73
+ end
74
+ end
75
+
76
+ def calculate_delay(iteration, response)
77
+ @delay_calculator.call(iteration, response)
78
+ end
79
+
80
+ private
81
+
82
+ # rubocop:disable Lint/RescueException
83
+ def try_request
84
+ err, res = nil
85
+
86
+ begin
87
+ res = yield
88
+ rescue Exception => e
89
+ err = e
90
+ end
91
+
92
+ [err, res]
93
+ end
94
+ # rubocop:enable Lint/RescueException
95
+
96
+ def retry_request?(req, err, res, attempt)
97
+ if @should_retry_proc
98
+ @should_retry_proc.call(req, err, res, attempt)
99
+ elsif err
100
+ retry_exception?(err)
101
+ else
102
+ retry_response?(res)
103
+ end
104
+ end
105
+
106
+ def retry_exception?(err)
107
+ @exception_classes.any? { |e| err.is_a?(e) }
108
+ end
109
+
110
+ def retry_response?(res)
111
+ return false unless @retry_statuses
112
+
113
+ response_status = res.status.to_i
114
+ retry_matchers = [@retry_statuses].flatten
115
+
116
+ retry_matchers.any? do |matcher|
117
+ case matcher
118
+ when Range then matcher.cover?(response_status)
119
+ when Numeric then matcher == response_status
120
+ else matcher.call(response_status)
121
+ end
122
+ end
123
+ end
124
+
125
+ def wait_for_retry_or_raise(req, err, res, attempt)
126
+ if attempt < @tries
127
+ @on_retry.call(req, err, res)
128
+ sleep calculate_delay(attempt, res)
129
+ else
130
+ res&.flush
131
+ raise out_of_retries_error(req, res, err)
132
+ end
133
+ end
134
+
135
+ # Builds OutOfRetriesError
136
+ #
137
+ # @param request [HTTP::Request]
138
+ # @param status [HTTP::Response, nil]
139
+ # @param exception [Exception, nil]
140
+ def out_of_retries_error(request, response, exception)
141
+ message = "#{request.verb.to_s.upcase} <#{request.uri}> failed"
142
+
143
+ message += " with #{response.status}" if response
144
+ message += ":#{exception}" if exception
145
+
146
+ HTTP::OutOfRetriesError.new(message).tap do |ex|
147
+ ex.cause = exception
148
+ ex.response = response
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -1,15 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
3
  require "io/wait"
5
4
 
6
5
  module HTTP
7
6
  module Timeout
8
7
  class Null
9
- extend Forwardable
10
-
11
- def_delegators :@socket, :close, :closed?
12
-
13
8
  attr_reader :options, :socket
14
9
 
15
10
  def initialize(options = {})
@@ -27,6 +22,14 @@ module HTTP
27
22
  @socket.connect
28
23
  end
29
24
 
25
+ def close
26
+ @socket&.close
27
+ end
28
+
29
+ def closed?
30
+ @socket&.closed?
31
+ end
32
+
30
33
  # Configures the SSL connection and starts the connection
31
34
  def start_tls(host, ssl_socket_class, ssl_context)
32
35
  @socket = ssl_socket_class.new(socket, ssl_context)
data/lib/http/uri.rb CHANGED
@@ -37,6 +37,9 @@ module HTTP
37
37
  # @private
38
38
  HTTPS_SCHEME = "https"
39
39
 
40
+ # @private
41
+ PERCENT_ENCODE = /[^\x21-\x7E]+/.freeze
42
+
40
43
  # @private
41
44
  NORMALIZER = lambda do |uri|
42
45
  uri = HTTP::URI.parse uri
@@ -44,8 +47,8 @@ module HTTP
44
47
  HTTP::URI.new(
45
48
  :scheme => uri.normalized_scheme,
46
49
  :authority => uri.normalized_authority,
47
- :path => uri.normalized_path,
48
- :query => uri.query,
50
+ :path => uri.path.empty? ? "/" : percent_encode(Addressable::URI.normalize_path(uri.path)),
51
+ :query => percent_encode(uri.query),
49
52
  :fragment => uri.normalized_fragment
50
53
  )
51
54
  end
@@ -71,6 +74,19 @@ module HTTP
71
74
  Addressable::URI.form_encode(form_values, sort)
72
75
  end
73
76
 
77
+ # Percent-encode all characters matching a regular expression.
78
+ #
79
+ # @param [String] string raw string
80
+ #
81
+ # @return [String] encoded value
82
+ #
83
+ # @private
84
+ def self.percent_encode(string)
85
+ string&.gsub(PERCENT_ENCODE) do |substr|
86
+ substr.encode(Encoding::UTF_8).bytes.map { |c| format("%%%02X", c) }.join
87
+ end
88
+ end
89
+
74
90
  # Creates an HTTP::URI instance from the given options
75
91
  #
76
92
  # @param [Hash, Addressable::URI] options_or_uri
data/lib/http/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- VERSION = "5.1.1"
4
+ VERSION = "5.3.0"
5
5
  end
data/lib/http.rb CHANGED
@@ -6,6 +6,7 @@ require "http/timeout/per_operation"
6
6
  require "http/timeout/global"
7
7
  require "http/chainable"
8
8
  require "http/client"
9
+ require "http/retriable/client"
9
10
  require "http/connection"
10
11
  require "http/options"
11
12
  require "http/feature"
@@ -261,6 +261,7 @@ RSpec.describe HTTP::Client do
261
261
 
262
262
  expect(HTTP::Request).to receive(:new) do |opts|
263
263
  expect(opts[:body]).to eq '{"foo":"bar"}'
264
+ expect(opts[:headers]["Content-Type"]).to eq "application/json; charset=utf-8"
264
265
  end
265
266
 
266
267
  client.get("http://example.com/", :json => {:foo => :bar})
@@ -8,11 +8,31 @@ RSpec.describe HTTP::Connection do
8
8
  :headers => {}
9
9
  )
10
10
  end
11
- let(:socket) { double(:connect => nil) }
11
+ let(:socket) { double(:connect => nil, :close => nil) }
12
12
  let(:timeout_class) { double(:new => socket) }
13
13
  let(:opts) { HTTP::Options.new(:timeout_class => timeout_class) }
14
14
  let(:connection) { HTTP::Connection.new(req, opts) }
15
15
 
16
+ describe "#initialize times out" do
17
+ let(:req) do
18
+ HTTP::Request.new(
19
+ :verb => :get,
20
+ :uri => "https://example.com/",
21
+ :headers => {}
22
+ )
23
+ end
24
+
25
+ before do
26
+ expect(socket).to receive(:start_tls).and_raise(HTTP::TimeoutError)
27
+ expect(socket).to receive(:closed?) { false }
28
+ expect(socket).to receive(:close)
29
+ end
30
+
31
+ it "closes the connection" do
32
+ expect { connection }.to raise_error(HTTP::TimeoutError)
33
+ end
34
+ end
35
+
16
36
  describe "#read_headers!" do
17
37
  before do
18
38
  connection.instance_variable_set(:@pending_response, true)
@@ -58,9 +78,11 @@ RSpec.describe HTTP::Connection do
58
78
  connection.read_headers!
59
79
  buffer = String.new
60
80
  while (s = connection.readpartial(3))
81
+ expect(connection.finished_request?).to be false if s != ""
61
82
  buffer << s
62
83
  end
63
84
  expect(buffer).to eq "1234567890"
85
+ expect(connection.finished_request?).to be true
64
86
  end
65
87
  end
66
88
  end
@@ -59,4 +59,23 @@ RSpec.describe HTTP::Features::Instrumentation do
59
59
  expect(instrumenter.output[:finish]).to eq(:response => response)
60
60
  end
61
61
  end
62
+
63
+ describe "logging errors" do
64
+ let(:request) do
65
+ HTTP::Request.new(
66
+ :verb => :post,
67
+ :uri => "https://example.com/",
68
+ :headers => {:accept => "application/json"},
69
+ :body => '{"hello": "world!"}'
70
+ )
71
+ end
72
+
73
+ let(:error) { HTTP::TimeoutError.new }
74
+
75
+ it "should log the error" do
76
+ feature.on_error(request, error)
77
+
78
+ expect(instrumenter.output[:finish]).to eq(:request => request, :error => error)
79
+ end
80
+ end
62
81
  end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe HTTP::Features::RaiseError do
4
+ subject(:feature) { described_class.new(ignore: ignore) }
5
+
6
+ let(:connection) { double }
7
+ let(:status) { 200 }
8
+ let(:ignore) { [] }
9
+
10
+ describe "#wrap_response" do
11
+ subject(:result) { feature.wrap_response(response) }
12
+
13
+ let(:response) do
14
+ HTTP::Response.new(
15
+ version: "1.1",
16
+ status: status,
17
+ headers: {},
18
+ connection: connection,
19
+ request: HTTP::Request.new(verb: :get, uri: "https://example.com")
20
+ )
21
+ end
22
+
23
+ context "when status is 200" do
24
+ it "returns original request" do
25
+ expect(result).to be response
26
+ end
27
+ end
28
+
29
+ context "when status is 399" do
30
+ let(:status) { 399 }
31
+
32
+ it "returns original request" do
33
+ expect(result).to be response
34
+ end
35
+ end
36
+
37
+ context "when status is 400" do
38
+ let(:status) { 400 }
39
+
40
+ it "raises" do
41
+ expect { result }.to raise_error(HTTP::StatusError, "Unexpected status code 400")
42
+ end
43
+ end
44
+
45
+ context "when status is 599" do
46
+ let(:status) { 599 }
47
+
48
+ it "raises" do
49
+ expect { result }.to raise_error(HTTP::StatusError, "Unexpected status code 599")
50
+ end
51
+ end
52
+
53
+ context "when error status is ignored" do
54
+ let(:status) { 500 }
55
+ let(:ignore) { [500] }
56
+
57
+ it "returns original request" do
58
+ expect(result).to be response
59
+ end
60
+ end
61
+ end
62
+ end