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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +67 -0
- data/.gitignore +6 -9
- data/.rspec +0 -4
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/metrics.yml +4 -0
- data/.rubocop/rspec.yml +9 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop.yml +9 -108
- data/.rubocop_todo.yml +219 -0
- data/.yardopts +1 -1
- data/CHANGELOG.md +67 -0
- data/{CHANGES.md → CHANGES_OLD.md} +358 -0
- data/Gemfile +19 -10
- data/LICENSE.txt +1 -1
- data/README.md +53 -85
- data/Rakefile +3 -11
- data/SECURITY.md +17 -0
- data/http.gemspec +15 -6
- data/lib/http/base64.rb +12 -0
- data/lib/http/chainable.rb +71 -41
- data/lib/http/client.rb +73 -52
- data/lib/http/connection.rb +28 -18
- data/lib/http/content_type.rb +12 -7
- data/lib/http/errors.rb +19 -0
- data/lib/http/feature.rb +18 -1
- data/lib/http/features/auto_deflate.rb +27 -6
- data/lib/http/features/auto_inflate.rb +32 -6
- data/lib/http/features/instrumentation.rb +69 -0
- data/lib/http/features/logging.rb +53 -0
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +22 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +69 -0
- data/lib/http/headers.rb +72 -49
- data/lib/http/mime_type/adapter.rb +3 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +31 -28
- data/lib/http/redirector.rb +56 -4
- data/lib/http/request/body.rb +31 -0
- data/lib/http/request/writer.rb +29 -9
- data/lib/http/request.rb +76 -41
- data/lib/http/response/body.rb +6 -4
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +78 -26
- data/lib/http/response/status.rb +4 -3
- data/lib/http/response.rb +45 -27
- data/lib/http/retriable/client.rb +37 -0
- data/lib/http/retriable/delay_calculator.rb +64 -0
- data/lib/http/retriable/errors.rb +14 -0
- data/lib/http/retriable/performer.rb +153 -0
- data/lib/http/timeout/global.rb +29 -47
- data/lib/http/timeout/null.rb +12 -8
- data/lib/http/timeout/per_operation.rb +32 -57
- data/lib/http/uri.rb +75 -1
- data/lib/http/version.rb +1 -1
- data/lib/http.rb +2 -2
- data/spec/lib/http/client_spec.rb +189 -36
- data/spec/lib/http/connection_spec.rb +31 -6
- data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
- data/spec/lib/http/features/instrumentation_spec.rb +81 -0
- data/spec/lib/http/features/logging_spec.rb +65 -0
- data/spec/lib/http/features/raise_error_spec.rb +62 -0
- data/spec/lib/http/headers/normalizer_spec.rb +52 -0
- data/spec/lib/http/headers_spec.rb +53 -18
- data/spec/lib/http/options/headers_spec.rb +6 -2
- data/spec/lib/http/options/merge_spec.rb +16 -16
- data/spec/lib/http/redirector_spec.rb +147 -3
- data/spec/lib/http/request/body_spec.rb +71 -4
- data/spec/lib/http/request/writer_spec.rb +45 -2
- data/spec/lib/http/request_spec.rb +11 -5
- data/spec/lib/http/response/body_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +74 -0
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +83 -7
- data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
- data/spec/lib/http/retriable/performer_spec.rb +302 -0
- data/spec/lib/http/uri/normalizer_spec.rb +95 -0
- data/spec/lib/http/uri_spec.rb +39 -0
- data/spec/lib/http_spec.rb +121 -68
- data/spec/regression_specs.rb +7 -0
- data/spec/spec_helper.rb +22 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server/servlet.rb +42 -11
- data/spec/support/dummy_server.rb +9 -8
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/http_handling_shared.rb +62 -66
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +66 -27
- data/.coveralls.yml +0 -1
- data/.ruby-version +0 -1
- data/.travis.yml +0 -36
data/lib/http/response/parser.rb
CHANGED
|
@@ -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
|
-
@
|
|
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
|
-
|
|
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
|
|
19
|
-
|
|
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
|
|
23
|
-
@
|
|
44
|
+
def headers?
|
|
45
|
+
@header_finished
|
|
24
46
|
end
|
|
25
47
|
|
|
26
|
-
def
|
|
27
|
-
@
|
|
48
|
+
def add_header(name, value)
|
|
49
|
+
@headers.add(name, value)
|
|
28
50
|
end
|
|
29
51
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
52
|
+
def mark_message_finished
|
|
53
|
+
@message_finished = true
|
|
54
|
+
end
|
|
33
55
|
|
|
34
|
-
def
|
|
35
|
-
@
|
|
56
|
+
def finished?
|
|
57
|
+
@message_finished
|
|
36
58
|
end
|
|
37
59
|
|
|
38
|
-
def
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
82
|
+
class Handler < LLHttp::Delegate
|
|
83
|
+
def initialize(target)
|
|
84
|
+
@target = target
|
|
85
|
+
super()
|
|
86
|
+
reset
|
|
87
|
+
end
|
|
63
88
|
|
|
64
|
-
|
|
65
|
-
|
|
89
|
+
def reset
|
|
90
|
+
@reading_header_value = false
|
|
91
|
+
@field_value = +""
|
|
92
|
+
@field = +""
|
|
93
|
+
end
|
|
66
94
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
95
|
+
def on_header_field(field)
|
|
96
|
+
append_header if @reading_header_value
|
|
97
|
+
@field << field
|
|
98
|
+
end
|
|
71
99
|
|
|
72
|
-
|
|
73
|
-
|
|
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
|
data/lib/http/response/status.rb
CHANGED
|
@@ -58,7 +58,7 @@ module HTTP
|
|
|
58
58
|
# SYMBOLS[418] # => :im_a_teapot
|
|
59
59
|
#
|
|
60
60
|
# @return [Hash<Fixnum => Symbol>]
|
|
61
|
-
SYMBOLS =
|
|
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 =
|
|
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 [
|
|
27
|
-
attr_reader :
|
|
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 [
|
|
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
|
-
@
|
|
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?(:
|
|
52
|
+
if opts.include?(:body)
|
|
53
|
+
@body = opts.fetch(:body)
|
|
54
|
+
else
|
|
50
55
|
connection = opts.fetch(:connection)
|
|
51
|
-
encoding
|
|
52
|
-
stream = body_stream_for(connection, opts)
|
|
56
|
+
encoding = opts[:encoding] || charset || default_encoding
|
|
53
57
|
|
|
54
|
-
@body = Response::Body.new(
|
|
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
|
|
64
|
+
def_delegator :@status, :reason
|
|
63
65
|
|
|
64
66
|
# @!method code
|
|
65
67
|
# @return (see HTTP::Response::Status#code)
|
|
66
|
-
def_delegator
|
|
68
|
+
def_delegator :@status, :code
|
|
67
69
|
|
|
68
70
|
# @!method to_s
|
|
69
71
|
# (see HTTP::Response::Body#to_s)
|
|
70
|
-
def_delegator
|
|
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
|
|
77
|
+
def_delegator :@body, :readpartial
|
|
76
78
|
|
|
77
79
|
# @!method connection
|
|
78
80
|
# (see HTTP::Response::Body#connection)
|
|
79
|
-
def_delegator
|
|
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 |
|
|
136
|
-
jar.parse(v, uri)
|
|
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]
|
|
152
|
-
#
|
|
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(
|
|
156
|
-
MimeType[
|
|
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
|
|
167
|
-
if
|
|
168
|
-
|
|
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
|
-
|
|
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,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
|