http 3.1.0 → 5.3.1

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 (93) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +67 -0
  3. data/.gitignore +6 -9
  4. data/.rspec +0 -4
  5. data/.rubocop/layout.yml +8 -0
  6. data/.rubocop/metrics.yml +4 -0
  7. data/.rubocop/rspec.yml +9 -0
  8. data/.rubocop/style.yml +32 -0
  9. data/.rubocop.yml +9 -108
  10. data/.rubocop_todo.yml +219 -0
  11. data/.yardopts +1 -1
  12. data/CHANGELOG.md +67 -0
  13. data/{CHANGES.md → CHANGES_OLD.md} +358 -0
  14. data/Gemfile +19 -10
  15. data/LICENSE.txt +1 -1
  16. data/README.md +53 -85
  17. data/Rakefile +3 -11
  18. data/SECURITY.md +17 -0
  19. data/http.gemspec +15 -6
  20. data/lib/http/base64.rb +12 -0
  21. data/lib/http/chainable.rb +71 -41
  22. data/lib/http/client.rb +73 -52
  23. data/lib/http/connection.rb +28 -18
  24. data/lib/http/content_type.rb +12 -7
  25. data/lib/http/errors.rb +19 -0
  26. data/lib/http/feature.rb +18 -1
  27. data/lib/http/features/auto_deflate.rb +27 -6
  28. data/lib/http/features/auto_inflate.rb +32 -6
  29. data/lib/http/features/instrumentation.rb +69 -0
  30. data/lib/http/features/logging.rb +53 -0
  31. data/lib/http/features/normalize_uri.rb +17 -0
  32. data/lib/http/features/raise_error.rb +22 -0
  33. data/lib/http/headers/known.rb +3 -0
  34. data/lib/http/headers/normalizer.rb +69 -0
  35. data/lib/http/headers.rb +72 -49
  36. data/lib/http/mime_type/adapter.rb +3 -1
  37. data/lib/http/mime_type/json.rb +1 -0
  38. data/lib/http/options.rb +31 -28
  39. data/lib/http/redirector.rb +56 -4
  40. data/lib/http/request/body.rb +31 -0
  41. data/lib/http/request/writer.rb +29 -9
  42. data/lib/http/request.rb +76 -41
  43. data/lib/http/response/body.rb +6 -4
  44. data/lib/http/response/inflater.rb +1 -1
  45. data/lib/http/response/parser.rb +78 -26
  46. data/lib/http/response/status.rb +4 -3
  47. data/lib/http/response.rb +45 -27
  48. data/lib/http/retriable/client.rb +37 -0
  49. data/lib/http/retriable/delay_calculator.rb +64 -0
  50. data/lib/http/retriable/errors.rb +14 -0
  51. data/lib/http/retriable/performer.rb +153 -0
  52. data/lib/http/timeout/global.rb +29 -47
  53. data/lib/http/timeout/null.rb +12 -8
  54. data/lib/http/timeout/per_operation.rb +32 -57
  55. data/lib/http/uri.rb +75 -1
  56. data/lib/http/version.rb +1 -1
  57. data/lib/http.rb +2 -2
  58. data/spec/lib/http/client_spec.rb +189 -36
  59. data/spec/lib/http/connection_spec.rb +31 -6
  60. data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
  61. data/spec/lib/http/features/instrumentation_spec.rb +81 -0
  62. data/spec/lib/http/features/logging_spec.rb +65 -0
  63. data/spec/lib/http/features/raise_error_spec.rb +62 -0
  64. data/spec/lib/http/headers/normalizer_spec.rb +52 -0
  65. data/spec/lib/http/headers_spec.rb +53 -18
  66. data/spec/lib/http/options/headers_spec.rb +6 -2
  67. data/spec/lib/http/options/merge_spec.rb +16 -16
  68. data/spec/lib/http/redirector_spec.rb +147 -3
  69. data/spec/lib/http/request/body_spec.rb +71 -4
  70. data/spec/lib/http/request/writer_spec.rb +45 -2
  71. data/spec/lib/http/request_spec.rb +11 -5
  72. data/spec/lib/http/response/body_spec.rb +5 -5
  73. data/spec/lib/http/response/parser_spec.rb +74 -0
  74. data/spec/lib/http/response/status_spec.rb +3 -3
  75. data/spec/lib/http/response_spec.rb +83 -7
  76. data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
  77. data/spec/lib/http/retriable/performer_spec.rb +302 -0
  78. data/spec/lib/http/uri/normalizer_spec.rb +95 -0
  79. data/spec/lib/http/uri_spec.rb +39 -0
  80. data/spec/lib/http_spec.rb +121 -68
  81. data/spec/regression_specs.rb +7 -0
  82. data/spec/spec_helper.rb +22 -21
  83. data/spec/support/black_hole.rb +1 -1
  84. data/spec/support/dummy_server/servlet.rb +42 -11
  85. data/spec/support/dummy_server.rb +9 -8
  86. data/spec/support/fuubar.rb +21 -0
  87. data/spec/support/http_handling_shared.rb +62 -66
  88. data/spec/support/simplecov.rb +19 -0
  89. data/spec/support/ssl_helper.rb +4 -4
  90. metadata +66 -27
  91. data/.coveralls.yml +0 -1
  92. data/.ruby-version +0 -1
  93. data/.travis.yml +0 -36
@@ -1,41 +1,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "llhttp"
4
+
3
5
  module HTTP
4
6
  class Response
7
+ # @api private
5
8
  class Parser
6
- attr_reader :headers
9
+ attr_reader :parser, :headers, :status_code, :http_version
7
10
 
8
11
  def initialize
9
- @parser = HTTP::Parser.new(self)
12
+ @handler = Handler.new(self)
13
+ @parser = LLHttp::Parser.new(@handler, :type => :response)
10
14
  reset
11
15
  end
12
16
 
17
+ def reset
18
+ @parser.reset
19
+ @handler.reset
20
+ @header_finished = false
21
+ @message_finished = false
22
+ @headers = Headers.new
23
+ @chunk = nil
24
+ @status_code = nil
25
+ @http_version = nil
26
+ end
27
+
13
28
  def add(data)
14
- @parser << data
29
+ parser << data
30
+
31
+ self
32
+ rescue LLHttp::Error => e
33
+ raise IOError, e.message
15
34
  end
35
+
16
36
  alias << add
17
37
 
18
- def headers?
19
- !!@headers
38
+ def mark_header_finished
39
+ @header_finished = true
40
+ @status_code = @parser.status_code
41
+ @http_version = "#{@parser.http_major}.#{@parser.http_minor}"
20
42
  end
21
43
 
22
- def http_version
23
- @parser.http_version.join(".")
44
+ def headers?
45
+ @header_finished
24
46
  end
25
47
 
26
- def status_code
27
- @parser.status_code
48
+ def add_header(name, value)
49
+ @headers.add(name, value)
28
50
  end
29
51
 
30
- #
31
- # HTTP::Parser callbacks
32
- #
52
+ def mark_message_finished
53
+ @message_finished = true
54
+ end
33
55
 
34
- def on_headers_complete(headers)
35
- @headers = headers
56
+ def finished?
57
+ @message_finished
36
58
  end
37
59
 
38
- def on_body(chunk)
60
+ def add_body(chunk)
39
61
  if @chunk
40
62
  @chunk << chunk
41
63
  else
@@ -57,20 +79,50 @@ module HTTP
57
79
  chunk
58
80
  end
59
81
 
60
- def on_message_complete
61
- @finished = true
62
- end
82
+ class Handler < LLHttp::Delegate
83
+ def initialize(target)
84
+ @target = target
85
+ super()
86
+ reset
87
+ end
63
88
 
64
- def reset
65
- @parser.reset!
89
+ def reset
90
+ @reading_header_value = false
91
+ @field_value = +""
92
+ @field = +""
93
+ end
66
94
 
67
- @finished = false
68
- @headers = nil
69
- @chunk = nil
70
- end
95
+ def on_header_field(field)
96
+ append_header if @reading_header_value
97
+ @field << field
98
+ end
71
99
 
72
- def finished?
73
- @finished
100
+ def on_header_value(value)
101
+ @reading_header_value = true
102
+ @field_value << value
103
+ end
104
+
105
+ def on_headers_complete
106
+ append_header if @reading_header_value
107
+ @target.mark_header_finished
108
+ end
109
+
110
+ def on_body(body)
111
+ @target.add_body(body)
112
+ end
113
+
114
+ def on_message_complete
115
+ @target.mark_message_finished
116
+ end
117
+
118
+ private
119
+
120
+ def append_header
121
+ @target.add_header(@field, @field_value)
122
+ @reading_header_value = false
123
+ @field_value = +""
124
+ @field = +""
125
+ end
74
126
  end
75
127
  end
76
128
  end
@@ -58,7 +58,7 @@ module HTTP
58
58
  # SYMBOLS[418] # => :im_a_teapot
59
59
  #
60
60
  # @return [Hash<Fixnum => Symbol>]
61
- SYMBOLS = Hash[REASONS.map { |k, v| [k, symbolize(v)] }].freeze
61
+ SYMBOLS = REASONS.transform_values { |v| symbolize(v) }.freeze
62
62
 
63
63
  # Reversed {SYMBOLS} map.
64
64
  #
@@ -69,7 +69,7 @@ module HTTP
69
69
  # SYMBOL_CODES[:im_a_teapot] # => 418
70
70
  #
71
71
  # @return [Hash<Symbol => Fixnum>]
72
- SYMBOL_CODES = Hash[SYMBOLS.map { |k, v| [v, k] }].freeze
72
+ SYMBOL_CODES = SYMBOLS.to_h { |k, v| [v, k] }.freeze
73
73
 
74
74
  # @return [Fixnum] status code
75
75
  attr_reader :code
@@ -132,7 +132,7 @@ module HTTP
132
132
  end
133
133
 
134
134
  SYMBOLS.each do |code, symbol|
135
- class_eval <<-RUBY, __FILE__, __LINE__
135
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
136
136
  def #{symbol}? # def bad_request?
137
137
  #{code} == code # 400 == code
138
138
  end # end
@@ -141,6 +141,7 @@ module HTTP
141
141
 
142
142
  def __setobj__(obj)
143
143
  raise TypeError, "Expected #{obj.inspect} to respond to #to_i" unless obj.respond_to? :to_i
144
+
144
145
  @code = obj.to_i
145
146
  end
146
147
 
data/lib/http/response.rb CHANGED
@@ -7,7 +7,6 @@ require "http/content_type"
7
7
  require "http/mime_type"
8
8
  require "http/response/status"
9
9
  require "http/response/inflater"
10
- require "http/uri"
11
10
  require "http/cookie_jar"
12
11
  require "time"
13
12
 
@@ -20,11 +19,14 @@ module HTTP
20
19
  # @return [Status]
21
20
  attr_reader :status
22
21
 
22
+ # @return [String]
23
+ attr_reader :version
24
+
23
25
  # @return [Body]
24
26
  attr_reader :body
25
27
 
26
- # @return [URI, nil]
27
- attr_reader :uri
28
+ # @return [Request]
29
+ attr_reader :request
28
30
 
29
31
  # @return [Hash]
30
32
  attr_reader :proxy_headers
@@ -38,45 +40,49 @@ module HTTP
38
40
  # @option opts [HTTP::Connection] :connection
39
41
  # @option opts [String] :encoding Encoding to use when reading body
40
42
  # @option opts [String] :body
41
- # @option opts [String] :uri
43
+ # @option opts [HTTP::Request] request The request this is in response to.
44
+ # @option opts [String] :uri (DEPRECATED) used to populate a missing request
42
45
  def initialize(opts)
43
46
  @version = opts.fetch(:version)
44
- @uri = HTTP::URI.parse(opts.fetch(:uri)) if opts.include? :uri
47
+ @request = init_request(opts)
45
48
  @status = HTTP::Response::Status.new(opts.fetch(:status))
46
49
  @headers = HTTP::Headers.coerce(opts[:headers] || {})
47
50
  @proxy_headers = HTTP::Headers.coerce(opts[:proxy_headers] || {})
48
51
 
49
- if opts.include?(:connection)
52
+ if opts.include?(:body)
53
+ @body = opts.fetch(:body)
54
+ else
50
55
  connection = opts.fetch(:connection)
51
- encoding = opts[:encoding] || charset || Encoding::BINARY
52
- stream = body_stream_for(connection, opts)
56
+ encoding = opts[:encoding] || charset || default_encoding
53
57
 
54
- @body = Response::Body.new(stream, :encoding => encoding)
55
- else
56
- @body = opts.fetch(:body)
58
+ @body = Response::Body.new(connection, :encoding => encoding)
57
59
  end
58
60
  end
59
61
 
60
62
  # @!method reason
61
63
  # @return (see HTTP::Response::Status#reason)
62
- def_delegator :status, :reason
64
+ def_delegator :@status, :reason
63
65
 
64
66
  # @!method code
65
67
  # @return (see HTTP::Response::Status#code)
66
- def_delegator :status, :code
68
+ def_delegator :@status, :code
67
69
 
68
70
  # @!method to_s
69
71
  # (see HTTP::Response::Body#to_s)
70
- def_delegator :body, :to_s
72
+ def_delegator :@body, :to_s
71
73
  alias to_str to_s
72
74
 
73
75
  # @!method readpartial
74
76
  # (see HTTP::Response::Body#readpartial)
75
- def_delegator :body, :readpartial
77
+ def_delegator :@body, :readpartial
76
78
 
77
79
  # @!method connection
78
80
  # (see HTTP::Response::Body#connection)
79
- def_delegator :body, :connection
81
+ def_delegator :@body, :connection
82
+
83
+ # @!method uri
84
+ # @return (see HTTP::Request#uri)
85
+ def_delegator :@request, :uri
80
86
 
81
87
  # Returns an Array ala Rack: `[status, headers, body]`
82
88
  #
@@ -132,8 +138,8 @@ module HTTP
132
138
  def_delegator :content_type, :charset
133
139
 
134
140
  def cookies
135
- @cookies ||= headers.each_with_object CookieJar.new do |(k, v), jar|
136
- jar.parse(v, uri) if k == Headers::SET_COOKIE
141
+ @cookies ||= headers.get(Headers::SET_COOKIE).each_with_object CookieJar.new do |v, jar|
142
+ jar.parse(v, uri)
137
143
  end
138
144
  end
139
145
 
@@ -148,12 +154,11 @@ module HTTP
148
154
 
149
155
  # Parse response body with corresponding MIME type adapter.
150
156
  #
151
- # @param [#to_s] as Parse as given MIME type
152
- # instead of the one determined from headers
153
- # @raise [HTTP::Error] if adapter not found
157
+ # @param type [#to_s] Parse as given MIME type.
158
+ # @raise (see MimeType.[])
154
159
  # @return [Object]
155
- def parse(as = nil)
156
- MimeType[as || mime_type].decode to_s
160
+ def parse(type = nil)
161
+ MimeType[type || mime_type].decode to_s
157
162
  end
158
163
 
159
164
  # Inspect a response
@@ -163,11 +168,24 @@ module HTTP
163
168
 
164
169
  private
165
170
 
166
- def body_stream_for(connection, opts)
167
- if opts[:auto_inflate]
168
- opts[:auto_inflate].stream_for(connection, self)
171
+ def default_encoding
172
+ return Encoding::UTF_8 if mime_type == "application/json"
173
+
174
+ Encoding::BINARY
175
+ end
176
+
177
+ # Initialize an HTTP::Request from options.
178
+ #
179
+ # @return [HTTP::Request]
180
+ def init_request(opts)
181
+ raise ArgumentError, ":uri is for backwards compatibilty and conflicts with :request" \
182
+ if opts[:request] && opts[:uri]
183
+
184
+ # For backwards compatibilty
185
+ if opts[:uri]
186
+ HTTP::Request.new(:uri => opts[:uri], :verb => :get)
169
187
  else
170
- connection
188
+ opts.fetch(:request)
171
189
  end
172
190
  end
173
191
  end
@@ -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