http 5.3.1 → 6.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +241 -41
- data/LICENSE.txt +1 -1
- data/README.md +110 -13
- data/UPGRADING.md +491 -0
- data/http.gemspec +32 -29
- data/lib/http/base64.rb +11 -1
- data/lib/http/chainable/helpers.rb +62 -0
- data/lib/http/chainable/verbs.rb +136 -0
- data/lib/http/chainable.rb +232 -136
- data/lib/http/client.rb +158 -127
- data/lib/http/connection/internals.rb +141 -0
- data/lib/http/connection.rb +126 -97
- data/lib/http/content_type.rb +61 -6
- data/lib/http/errors.rb +25 -1
- data/lib/http/feature.rb +65 -5
- data/lib/http/features/auto_deflate.rb +124 -17
- data/lib/http/features/auto_inflate.rb +38 -15
- data/lib/http/features/caching/entry.rb +178 -0
- data/lib/http/features/caching/in_memory_store.rb +63 -0
- data/lib/http/features/caching.rb +216 -0
- data/lib/http/features/digest_auth.rb +234 -0
- data/lib/http/features/instrumentation.rb +97 -17
- data/lib/http/features/logging.rb +183 -5
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +18 -3
- data/lib/http/form_data/composite_io.rb +106 -0
- data/lib/http/form_data/file.rb +95 -0
- data/lib/http/form_data/multipart/param.rb +62 -0
- data/lib/http/form_data/multipart.rb +106 -0
- data/lib/http/form_data/part.rb +52 -0
- data/lib/http/form_data/readable.rb +58 -0
- data/lib/http/form_data/urlencoded.rb +175 -0
- data/lib/http/form_data/version.rb +8 -0
- data/lib/http/form_data.rb +102 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +17 -36
- data/lib/http/headers.rb +172 -65
- data/lib/http/mime_type/adapter.rb +24 -9
- data/lib/http/mime_type/json.rb +19 -4
- data/lib/http/mime_type.rb +21 -3
- data/lib/http/options/definitions.rb +189 -0
- data/lib/http/options.rb +172 -125
- data/lib/http/redirector.rb +80 -75
- data/lib/http/request/body.rb +87 -6
- data/lib/http/request/builder.rb +184 -0
- data/lib/http/request/proxy.rb +83 -0
- data/lib/http/request/writer.rb +76 -16
- data/lib/http/request.rb +214 -98
- data/lib/http/response/body.rb +103 -18
- data/lib/http/response/inflater.rb +35 -7
- data/lib/http/response/parser.rb +98 -4
- data/lib/http/response/status/reasons.rb +2 -4
- data/lib/http/response/status.rb +141 -31
- data/lib/http/response.rb +219 -61
- data/lib/http/retriable/delay_calculator.rb +38 -11
- data/lib/http/retriable/errors.rb +21 -0
- data/lib/http/retriable/performer.rb +82 -38
- data/lib/http/session.rb +280 -0
- data/lib/http/timeout/global.rb +147 -34
- data/lib/http/timeout/null.rb +155 -9
- data/lib/http/timeout/per_operation.rb +139 -18
- data/lib/http/uri/normalizer.rb +82 -0
- data/lib/http/uri/parsing.rb +182 -0
- data/lib/http/uri.rb +289 -124
- data/lib/http/version.rb +2 -1
- data/lib/http.rb +11 -2
- data/sig/deps.rbs +122 -0
- data/sig/http.rbs +1619 -0
- data/test/http/base64_test.rb +28 -0
- data/test/http/client_test.rb +739 -0
- data/test/http/connection_test.rb +1533 -0
- data/test/http/content_type_test.rb +190 -0
- data/test/http/errors_test.rb +28 -0
- data/test/http/feature_test.rb +49 -0
- data/test/http/features/auto_deflate_test.rb +317 -0
- data/test/http/features/auto_inflate_test.rb +213 -0
- data/test/http/features/caching_test.rb +942 -0
- data/test/http/features/digest_auth_test.rb +996 -0
- data/test/http/features/instrumentation_test.rb +246 -0
- data/test/http/features/logging_test.rb +654 -0
- data/test/http/features/normalize_uri_test.rb +41 -0
- data/test/http/features/raise_error_test.rb +77 -0
- data/test/http/form_data/composite_io_test.rb +215 -0
- data/test/http/form_data/file_test.rb +255 -0
- data/test/http/form_data/fixtures/the-http-gem.info +1 -0
- data/test/http/form_data/multipart_test.rb +303 -0
- data/test/http/form_data/part_test.rb +90 -0
- data/test/http/form_data/urlencoded_test.rb +164 -0
- data/test/http/form_data_test.rb +232 -0
- data/test/http/headers/normalizer_test.rb +93 -0
- data/test/http/headers_test.rb +888 -0
- data/test/http/mime_type/json_test.rb +39 -0
- data/test/http/mime_type_test.rb +150 -0
- data/test/http/options/base_uri_test.rb +148 -0
- data/test/http/options/body_test.rb +21 -0
- data/test/http/options/features_test.rb +38 -0
- data/test/http/options/form_test.rb +21 -0
- data/test/http/options/headers_test.rb +32 -0
- data/test/http/options/json_test.rb +21 -0
- data/test/http/options/merge_test.rb +78 -0
- data/test/http/options/new_test.rb +37 -0
- data/test/http/options/proxy_test.rb +32 -0
- data/test/http/options_test.rb +575 -0
- data/test/http/redirector_test.rb +639 -0
- data/test/http/request/body_test.rb +318 -0
- data/test/http/request/builder_test.rb +623 -0
- data/test/http/request/writer_test.rb +391 -0
- data/test/http/request_test.rb +1733 -0
- data/test/http/response/body_test.rb +292 -0
- data/test/http/response/parser_test.rb +105 -0
- data/test/http/response/status_test.rb +322 -0
- data/test/http/response_test.rb +502 -0
- data/test/http/retriable/delay_calculator_test.rb +194 -0
- data/test/http/retriable/errors_test.rb +71 -0
- data/test/http/retriable/performer_test.rb +551 -0
- data/test/http/session_test.rb +424 -0
- data/test/http/timeout/global_test.rb +239 -0
- data/test/http/timeout/null_test.rb +218 -0
- data/test/http/timeout/per_operation_test.rb +220 -0
- data/test/http/uri/normalizer_test.rb +89 -0
- data/test/http/uri_test.rb +1140 -0
- data/test/http/version_test.rb +15 -0
- data/test/http_test.rb +818 -0
- data/test/regression_tests.rb +27 -0
- data/test/support/dummy_server/encoding_routes.rb +47 -0
- data/test/support/dummy_server/routes.rb +201 -0
- data/test/support/dummy_server/servlet.rb +81 -0
- data/test/support/dummy_server.rb +200 -0
- data/{spec → test}/support/fakeio.rb +2 -2
- data/test/support/http_handling_shared/connection_reuse_tests.rb +97 -0
- data/test/support/http_handling_shared/timeout_tests.rb +134 -0
- data/test/support/http_handling_shared.rb +11 -0
- data/test/support/proxy_server.rb +207 -0
- data/test/support/servers/runner.rb +67 -0
- data/{spec → test}/support/simplecov.rb +11 -2
- data/test/support/ssl_helper.rb +108 -0
- data/test/test_helper.rb +38 -0
- metadata +108 -168
- data/.github/workflows/ci.yml +0 -67
- data/.gitignore +0 -15
- data/.rspec +0 -1
- data/.rubocop/layout.yml +0 -8
- data/.rubocop/metrics.yml +0 -4
- data/.rubocop/rspec.yml +0 -9
- data/.rubocop/style.yml +0 -32
- data/.rubocop.yml +0 -11
- data/.rubocop_todo.yml +0 -219
- data/.yardopts +0 -2
- data/CHANGES_OLD.md +0 -1002
- data/Gemfile +0 -51
- data/Guardfile +0 -18
- data/Rakefile +0 -64
- data/lib/http/headers/mixin.rb +0 -34
- data/lib/http/retriable/client.rb +0 -37
- data/logo.png +0 -0
- data/spec/lib/http/client_spec.rb +0 -556
- data/spec/lib/http/connection_spec.rb +0 -88
- data/spec/lib/http/content_type_spec.rb +0 -47
- data/spec/lib/http/features/auto_deflate_spec.rb +0 -77
- data/spec/lib/http/features/auto_inflate_spec.rb +0 -86
- data/spec/lib/http/features/instrumentation_spec.rb +0 -81
- data/spec/lib/http/features/logging_spec.rb +0 -65
- data/spec/lib/http/features/raise_error_spec.rb +0 -62
- data/spec/lib/http/headers/mixin_spec.rb +0 -36
- data/spec/lib/http/headers/normalizer_spec.rb +0 -52
- data/spec/lib/http/headers_spec.rb +0 -527
- data/spec/lib/http/options/body_spec.rb +0 -15
- data/spec/lib/http/options/features_spec.rb +0 -33
- data/spec/lib/http/options/form_spec.rb +0 -15
- data/spec/lib/http/options/headers_spec.rb +0 -24
- data/spec/lib/http/options/json_spec.rb +0 -15
- data/spec/lib/http/options/merge_spec.rb +0 -68
- data/spec/lib/http/options/new_spec.rb +0 -30
- data/spec/lib/http/options/proxy_spec.rb +0 -20
- data/spec/lib/http/options_spec.rb +0 -13
- data/spec/lib/http/redirector_spec.rb +0 -530
- data/spec/lib/http/request/body_spec.rb +0 -211
- data/spec/lib/http/request/writer_spec.rb +0 -121
- data/spec/lib/http/request_spec.rb +0 -234
- data/spec/lib/http/response/body_spec.rb +0 -85
- data/spec/lib/http/response/parser_spec.rb +0 -74
- data/spec/lib/http/response/status_spec.rb +0 -253
- data/spec/lib/http/response_spec.rb +0 -262
- data/spec/lib/http/retriable/delay_calculator_spec.rb +0 -69
- data/spec/lib/http/retriable/performer_spec.rb +0 -302
- data/spec/lib/http/uri/normalizer_spec.rb +0 -95
- data/spec/lib/http/uri_spec.rb +0 -71
- data/spec/lib/http_spec.rb +0 -535
- data/spec/regression_specs.rb +0 -24
- data/spec/spec_helper.rb +0 -89
- data/spec/support/black_hole.rb +0 -13
- data/spec/support/dummy_server/servlet.rb +0 -203
- data/spec/support/dummy_server.rb +0 -44
- data/spec/support/fuubar.rb +0 -21
- data/spec/support/http_handling_shared.rb +0 -190
- data/spec/support/proxy_server.rb +0 -39
- data/spec/support/servers/config.rb +0 -11
- data/spec/support/servers/runner.rb +0 -19
- data/spec/support/ssl_helper.rb +0 -104
- /data/{spec → test}/support/capture_warning.rb +0 -0
|
@@ -2,17 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
module HTTP
|
|
4
4
|
module Retriable
|
|
5
|
+
# Calculates retry delays with support for Retry-After headers
|
|
5
6
|
# @api private
|
|
6
7
|
class DelayCalculator
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# Initializes the delay calculator
|
|
9
|
+
#
|
|
10
|
+
# @param [#call, Numeric, nil] delay delay value or proc
|
|
11
|
+
# @param [#to_f] max_delay maximum delay cap
|
|
12
|
+
# @api private
|
|
13
|
+
# @return [HTTP::Retriable::DelayCalculator]
|
|
14
|
+
def initialize(delay: nil, max_delay: Float::MAX)
|
|
15
|
+
@max_delay = Float(max_delay)
|
|
16
|
+
if delay.respond_to?(:call)
|
|
17
|
+
@delay_proc = delay
|
|
11
18
|
else
|
|
12
19
|
@delay = delay
|
|
13
20
|
end
|
|
14
21
|
end
|
|
15
22
|
|
|
23
|
+
# Calculates delay for the given iteration
|
|
24
|
+
#
|
|
25
|
+
# @param [Integer] iteration
|
|
26
|
+
# @param [HTTP::Response, nil] response
|
|
27
|
+
# @api private
|
|
28
|
+
# @return [Numeric]
|
|
16
29
|
def call(iteration, response)
|
|
17
30
|
delay = if response && (retry_header = response.headers["Retry-After"])
|
|
18
31
|
delay_from_retry_header(retry_header)
|
|
@@ -20,9 +33,10 @@ module HTTP
|
|
|
20
33
|
calculate_delay_from_iteration(iteration)
|
|
21
34
|
end
|
|
22
35
|
|
|
23
|
-
|
|
36
|
+
ensure_delay_in_bounds(delay)
|
|
24
37
|
end
|
|
25
38
|
|
|
39
|
+
# Pattern matching RFC 2822 formatted dates in Retry-After headers
|
|
26
40
|
RFC2822_DATE_REGEX = /^
|
|
27
41
|
(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat),\s+
|
|
28
42
|
(?:0[1-9]|[1-2]?[0-9]|3[01])\s+
|
|
@@ -32,18 +46,26 @@ module HTTP
|
|
|
32
46
|
GMT
|
|
33
47
|
$/x
|
|
34
48
|
|
|
35
|
-
#
|
|
36
|
-
#
|
|
49
|
+
# Parses delay from Retry-After header value
|
|
50
|
+
#
|
|
51
|
+
# @param [String] value
|
|
52
|
+
# @api private
|
|
53
|
+
# @return [Numeric]
|
|
37
54
|
def delay_from_retry_header(value)
|
|
38
|
-
value = value.
|
|
55
|
+
value = String(value).strip
|
|
39
56
|
|
|
40
57
|
case value
|
|
41
58
|
when RFC2822_DATE_REGEX then DateTime.rfc2822(value).to_time - Time.now.utc
|
|
42
|
-
when
|
|
59
|
+
when /\A\d+$/ then value.to_i
|
|
43
60
|
else 0
|
|
44
61
|
end
|
|
45
62
|
end
|
|
46
63
|
|
|
64
|
+
# Calculates delay based on iteration number
|
|
65
|
+
#
|
|
66
|
+
# @param [Integer] iteration
|
|
67
|
+
# @api private
|
|
68
|
+
# @return [Numeric]
|
|
47
69
|
def calculate_delay_from_iteration(iteration)
|
|
48
70
|
if @delay_proc
|
|
49
71
|
@delay_proc.call(iteration)
|
|
@@ -56,8 +78,13 @@ module HTTP
|
|
|
56
78
|
end
|
|
57
79
|
end
|
|
58
80
|
|
|
59
|
-
|
|
60
|
-
|
|
81
|
+
# Clamps delay to configured bounds
|
|
82
|
+
#
|
|
83
|
+
# @param [Numeric] delay
|
|
84
|
+
# @api private
|
|
85
|
+
# @return [Numeric]
|
|
86
|
+
def ensure_delay_in_bounds(delay)
|
|
87
|
+
Float(delay.clamp(0, @max_delay))
|
|
61
88
|
end
|
|
62
89
|
end
|
|
63
90
|
end
|
|
@@ -3,10 +3,31 @@
|
|
|
3
3
|
module HTTP
|
|
4
4
|
# Retriable performance ran out of attempts
|
|
5
5
|
class OutOfRetriesError < Error
|
|
6
|
+
# The last response received before failure
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# error.response
|
|
10
|
+
#
|
|
11
|
+
# @return [HTTP::Response, nil] the last response received
|
|
12
|
+
# @api public
|
|
6
13
|
attr_accessor :response
|
|
7
14
|
|
|
15
|
+
# Set the underlying exception
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# error.cause = original_error
|
|
19
|
+
#
|
|
20
|
+
# @return [Exception, nil]
|
|
21
|
+
# @api public
|
|
8
22
|
attr_writer :cause
|
|
9
23
|
|
|
24
|
+
# Returns the cause of the error
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# error.cause
|
|
28
|
+
#
|
|
29
|
+
# @api public
|
|
30
|
+
# @return [Exception, nil]
|
|
10
31
|
def cause
|
|
11
32
|
@cause || super
|
|
12
33
|
end
|
|
@@ -6,6 +6,7 @@ require "http/retriable/delay_calculator"
|
|
|
6
6
|
require "openssl"
|
|
7
7
|
|
|
8
8
|
module HTTP
|
|
9
|
+
# Retry logic for failed HTTP requests
|
|
9
10
|
module Retriable
|
|
10
11
|
# Request performing watchdog.
|
|
11
12
|
# @api private
|
|
@@ -23,62 +24,87 @@ module HTTP
|
|
|
23
24
|
IOError
|
|
24
25
|
].freeze
|
|
25
26
|
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# @
|
|
29
|
-
# @
|
|
30
|
-
# @
|
|
31
|
-
# @
|
|
32
|
-
# @
|
|
33
|
-
# @
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@
|
|
40
|
-
@
|
|
27
|
+
# Create a new retry performer
|
|
28
|
+
#
|
|
29
|
+
# @param [#to_i] tries maximum number of attempts
|
|
30
|
+
# @param [#call, #to_i, nil] delay delay between retries
|
|
31
|
+
# @param [Array<Exception>] exceptions exception classes to retry
|
|
32
|
+
# @param [Array<#to_i>, nil] retry_statuses status codes to retry
|
|
33
|
+
# @param [#call] on_retry callback invoked on each retry
|
|
34
|
+
# @param [#to_f] max_delay maximum delay between retries
|
|
35
|
+
# @param [#call, nil] should_retry custom retry predicate
|
|
36
|
+
# @api private
|
|
37
|
+
# @return [HTTP::Retriable::Performer]
|
|
38
|
+
def initialize(tries: 5, delay: nil, exceptions: RETRIABLE_ERRORS, retry_statuses: nil,
|
|
39
|
+
on_retry: ->(*_args) {}, max_delay: Float::MAX, should_retry: nil)
|
|
40
|
+
@exception_classes = exceptions
|
|
41
|
+
@retry_statuses = retry_statuses
|
|
42
|
+
@tries = tries.to_i
|
|
43
|
+
@on_retry = on_retry
|
|
44
|
+
@should_retry_proc = should_retry
|
|
45
|
+
@delay_calculator = DelayCalculator.new(delay: delay, max_delay: max_delay)
|
|
41
46
|
end
|
|
42
47
|
|
|
43
|
-
#
|
|
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
|
+
# Execute request with retry logic
|
|
48
49
|
#
|
|
49
50
|
# @see #initialize
|
|
51
|
+
# @return [HTTP::Response]
|
|
50
52
|
# @api private
|
|
51
53
|
def perform(client, req, &block)
|
|
52
54
|
1.upto(Float::INFINITY) do |attempt| # infinite loop with index
|
|
53
55
|
err, res = try_request(&block)
|
|
54
56
|
|
|
55
57
|
if retry_request?(req, err, res, attempt)
|
|
56
|
-
|
|
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
|
|
58
|
+
retry_attempt(client, req, err, res, attempt)
|
|
67
59
|
elsif err
|
|
68
|
-
client
|
|
69
|
-
raise err
|
|
60
|
+
finish_attempt(client, err)
|
|
70
61
|
elsif res
|
|
71
62
|
return res
|
|
72
63
|
end
|
|
73
64
|
end
|
|
74
65
|
end
|
|
75
66
|
|
|
67
|
+
# Calculates delay between retries
|
|
68
|
+
#
|
|
69
|
+
# @param [Integer] iteration
|
|
70
|
+
# @param [HTTP::Response, nil] response
|
|
71
|
+
# @api private
|
|
72
|
+
# @return [Numeric]
|
|
76
73
|
def calculate_delay(iteration, response)
|
|
77
74
|
@delay_calculator.call(iteration, response)
|
|
78
75
|
end
|
|
79
76
|
|
|
80
77
|
private
|
|
81
78
|
|
|
79
|
+
# Executes a single retry attempt
|
|
80
|
+
#
|
|
81
|
+
# @api private
|
|
82
|
+
# @return [void]
|
|
83
|
+
def retry_attempt(client, req, err, res, attempt)
|
|
84
|
+
# Some servers support Keep-Alive on any response. Thus we should
|
|
85
|
+
# flush response before retry, to avoid state error (when socket
|
|
86
|
+
# has pending response data and we try to write new request).
|
|
87
|
+
# Alternatively, as we don't need response body here at all, we
|
|
88
|
+
# are going to close client, effectively closing underlying socket
|
|
89
|
+
# and resetting client's state.
|
|
90
|
+
wait_for_retry_or_raise(req, err, res, attempt)
|
|
91
|
+
ensure
|
|
92
|
+
client.close
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Closes client and raises the error
|
|
96
|
+
#
|
|
97
|
+
# @api private
|
|
98
|
+
# @return [void]
|
|
99
|
+
def finish_attempt(client, err)
|
|
100
|
+
client.close
|
|
101
|
+
raise err
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Attempts to execute the request block
|
|
105
|
+
#
|
|
106
|
+
# @api private
|
|
107
|
+
# @return [Array]
|
|
82
108
|
# rubocop:disable Lint/RescueException
|
|
83
109
|
def try_request
|
|
84
110
|
err, res = nil
|
|
@@ -93,6 +119,10 @@ module HTTP
|
|
|
93
119
|
end
|
|
94
120
|
# rubocop:enable Lint/RescueException
|
|
95
121
|
|
|
122
|
+
# Checks whether the request should be retried
|
|
123
|
+
#
|
|
124
|
+
# @api private
|
|
125
|
+
# @return [Boolean]
|
|
96
126
|
def retry_request?(req, err, res, attempt)
|
|
97
127
|
if @should_retry_proc
|
|
98
128
|
@should_retry_proc.call(req, err, res, attempt)
|
|
@@ -103,14 +133,22 @@ module HTTP
|
|
|
103
133
|
end
|
|
104
134
|
end
|
|
105
135
|
|
|
136
|
+
# Checks whether the exception is retriable
|
|
137
|
+
#
|
|
138
|
+
# @api private
|
|
139
|
+
# @return [Boolean]
|
|
106
140
|
def retry_exception?(err)
|
|
107
141
|
@exception_classes.any? { |e| err.is_a?(e) }
|
|
108
142
|
end
|
|
109
143
|
|
|
144
|
+
# Checks whether the response status warrants retry
|
|
145
|
+
#
|
|
146
|
+
# @api private
|
|
147
|
+
# @return [Boolean]
|
|
110
148
|
def retry_response?(res)
|
|
111
149
|
return false unless @retry_statuses
|
|
112
150
|
|
|
113
|
-
response_status = res.status
|
|
151
|
+
response_status = Integer(res.status)
|
|
114
152
|
retry_matchers = [@retry_statuses].flatten
|
|
115
153
|
|
|
116
154
|
retry_matchers.any? do |matcher|
|
|
@@ -122,10 +160,14 @@ module HTTP
|
|
|
122
160
|
end
|
|
123
161
|
end
|
|
124
162
|
|
|
163
|
+
# Waits for retry delay or raises if out of attempts
|
|
164
|
+
#
|
|
165
|
+
# @api private
|
|
166
|
+
# @return [void]
|
|
125
167
|
def wait_for_retry_or_raise(req, err, res, attempt)
|
|
126
168
|
if attempt < @tries
|
|
127
169
|
@on_retry.call(req, err, res)
|
|
128
|
-
sleep
|
|
170
|
+
sleep(calculate_delay(attempt, res))
|
|
129
171
|
else
|
|
130
172
|
res&.flush
|
|
131
173
|
raise out_of_retries_error(req, res, err)
|
|
@@ -135,15 +177,17 @@ module HTTP
|
|
|
135
177
|
# Builds OutOfRetriesError
|
|
136
178
|
#
|
|
137
179
|
# @param request [HTTP::Request]
|
|
138
|
-
# @param
|
|
180
|
+
# @param response [HTTP::Response, nil]
|
|
139
181
|
# @param exception [Exception, nil]
|
|
182
|
+
# @api private
|
|
183
|
+
# @return [HTTP::OutOfRetriesError]
|
|
140
184
|
def out_of_retries_error(request, response, exception)
|
|
141
|
-
message = "
|
|
185
|
+
message = format("%s <%s> failed", String(request.verb).upcase, request.uri)
|
|
142
186
|
|
|
143
187
|
message += " with #{response.status}" if response
|
|
144
188
|
message += ":#{exception}" if exception
|
|
145
189
|
|
|
146
|
-
|
|
190
|
+
OutOfRetriesError.new(message).tap do |ex|
|
|
147
191
|
ex.cause = exception
|
|
148
192
|
ex.response = response
|
|
149
193
|
end
|
data/lib/http/session.rb
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
require "http/cookie_jar"
|
|
6
|
+
require "http/headers"
|
|
7
|
+
require "http/redirector"
|
|
8
|
+
require "http/request/builder"
|
|
9
|
+
|
|
10
|
+
module HTTP
|
|
11
|
+
# Thread-safe options builder for configuring HTTP requests.
|
|
12
|
+
#
|
|
13
|
+
# Session objects are returned by all chainable configuration methods
|
|
14
|
+
# (e.g., {Chainable#headers}, {Chainable#timeout}, {Chainable#cookies}).
|
|
15
|
+
# They hold an immutable {Options} object and create a new {Client}
|
|
16
|
+
# for each request, making them safe to share across threads.
|
|
17
|
+
#
|
|
18
|
+
# When configured for persistent connections (via {Chainable#persistent}),
|
|
19
|
+
# the session maintains a pool of {Client} instances keyed by origin,
|
|
20
|
+
# enabling connection reuse within the same origin and transparent
|
|
21
|
+
# cross-origin redirect handling.
|
|
22
|
+
#
|
|
23
|
+
# @example Reuse a configured session across threads
|
|
24
|
+
# session = HTTP.headers("Accept" => "application/json").timeout(10)
|
|
25
|
+
# threads = 5.times.map do
|
|
26
|
+
# Thread.new { session.get("https://example.com") }
|
|
27
|
+
# end
|
|
28
|
+
# threads.each(&:join)
|
|
29
|
+
#
|
|
30
|
+
# @example Persistent session with cross-origin redirects
|
|
31
|
+
# HTTP.persistent("https://example.com").follow do |http|
|
|
32
|
+
# http.get("/redirect-to-other-domain") # follows cross-origin redirect
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# @see Chainable
|
|
36
|
+
# @see Client
|
|
37
|
+
class Session
|
|
38
|
+
extend Forwardable
|
|
39
|
+
include Chainable
|
|
40
|
+
|
|
41
|
+
# @!method persistent?
|
|
42
|
+
# Indicate whether the session has persistent connection options
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# session = HTTP::Session.new(persistent: "http://example.com")
|
|
46
|
+
# session.persistent?
|
|
47
|
+
#
|
|
48
|
+
# @see Options#persistent?
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
# @api public
|
|
51
|
+
def_delegator :default_options, :persistent?
|
|
52
|
+
|
|
53
|
+
# Initialize a new Session
|
|
54
|
+
#
|
|
55
|
+
# @example
|
|
56
|
+
# session = HTTP::Session.new(headers: {"Accept" => "application/json"})
|
|
57
|
+
#
|
|
58
|
+
# @param default_options [HTTP::Options, nil] existing options instance
|
|
59
|
+
# @param clients [Hash, nil] shared connection pool (internal use)
|
|
60
|
+
# @param options [Hash] keyword options (see HTTP::Options#initialize)
|
|
61
|
+
# @return [HTTP::Session] a new session instance
|
|
62
|
+
# @api public
|
|
63
|
+
def initialize(default_options = nil, clients: nil, **)
|
|
64
|
+
@default_options = HTTP::Options.new(default_options, **)
|
|
65
|
+
@clients = clients || {}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Close all persistent connections held by this session
|
|
69
|
+
#
|
|
70
|
+
# When the session is persistent, this closes every pooled {Client}
|
|
71
|
+
# and clears the pool. Safe to call on non-persistent sessions (no-op).
|
|
72
|
+
#
|
|
73
|
+
# @example
|
|
74
|
+
# session = HTTP.persistent("https://example.com")
|
|
75
|
+
# session.get("/")
|
|
76
|
+
# session.close
|
|
77
|
+
#
|
|
78
|
+
# @return [void]
|
|
79
|
+
# @api public
|
|
80
|
+
def close
|
|
81
|
+
@clients.each_value(&:close)
|
|
82
|
+
@clients.clear
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Make an HTTP request
|
|
86
|
+
#
|
|
87
|
+
# For non-persistent sessions a fresh {Client} is created for each
|
|
88
|
+
# request, ensuring thread safety. For persistent sessions the pooled
|
|
89
|
+
# {Client} for the request's origin is reused.
|
|
90
|
+
#
|
|
91
|
+
# Manages cookies across redirect hops when following redirects.
|
|
92
|
+
#
|
|
93
|
+
# @example Without a block
|
|
94
|
+
# session = HTTP::Session.new
|
|
95
|
+
# session.request(:get, "https://example.com")
|
|
96
|
+
#
|
|
97
|
+
# @example With a block (auto-closes connection)
|
|
98
|
+
# session = HTTP::Session.new
|
|
99
|
+
# session.request(:get, "https://example.com") { |res| res.status }
|
|
100
|
+
#
|
|
101
|
+
# @param verb [Symbol] the HTTP method
|
|
102
|
+
# @param uri [#to_s] the URI to request
|
|
103
|
+
# @yieldparam response [HTTP::Response] the response
|
|
104
|
+
# @return [HTTP::Response, Object] the response, or block return value
|
|
105
|
+
# @api public
|
|
106
|
+
def request(verb, uri,
|
|
107
|
+
headers: nil, params: nil, form: nil, json: nil, body: nil,
|
|
108
|
+
response: nil, encoding: nil, follow: nil, ssl: nil, ssl_context: nil,
|
|
109
|
+
proxy: nil, nodelay: nil, features: nil, retriable: nil,
|
|
110
|
+
socket_class: nil, ssl_socket_class: nil, timeout_class: nil,
|
|
111
|
+
timeout_options: nil, keep_alive_timeout: nil, base_uri: nil, persistent: nil, &block)
|
|
112
|
+
merged = default_options.merge(
|
|
113
|
+
{ headers: headers, params: params, form: form, json: json, body: body,
|
|
114
|
+
response: response, encoding: encoding, follow: follow, ssl: ssl,
|
|
115
|
+
ssl_context: ssl_context, proxy: proxy, nodelay: nodelay, features: features,
|
|
116
|
+
retriable: retriable, socket_class: socket_class, ssl_socket_class: ssl_socket_class,
|
|
117
|
+
timeout_class: timeout_class, timeout_options: timeout_options,
|
|
118
|
+
keep_alive_timeout: keep_alive_timeout, base_uri: base_uri, persistent: persistent }.compact
|
|
119
|
+
)
|
|
120
|
+
client = persistent? ? nil : make_client(default_options)
|
|
121
|
+
res = perform_request(client, verb, uri, merged)
|
|
122
|
+
|
|
123
|
+
return res unless block
|
|
124
|
+
|
|
125
|
+
yield res
|
|
126
|
+
ensure
|
|
127
|
+
if block
|
|
128
|
+
persistent? ? close : client&.close
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Create a new session with the given options
|
|
135
|
+
#
|
|
136
|
+
# When the current session is persistent, the child session shares the
|
|
137
|
+
# same connection pool so that chaining methods like {Chainable#headers}
|
|
138
|
+
# or {Chainable#auth} do not break connection reuse.
|
|
139
|
+
#
|
|
140
|
+
# @param options [HTTP::Options] options for the new session
|
|
141
|
+
# @return [HTTP::Session]
|
|
142
|
+
# @api private
|
|
143
|
+
def branch(options)
|
|
144
|
+
if persistent?
|
|
145
|
+
self.class.new(options, clients: @clients)
|
|
146
|
+
else
|
|
147
|
+
self.class.new(options)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Execute a request with cookie management
|
|
152
|
+
#
|
|
153
|
+
# @param client [HTTP::Client, nil] the client (nil when persistent; looked up from pool)
|
|
154
|
+
# @param verb [Symbol] the HTTP method
|
|
155
|
+
# @param uri [#to_s] the URI to request
|
|
156
|
+
# @param merged [HTTP::Options] the merged options
|
|
157
|
+
# @return [HTTP::Response] the response
|
|
158
|
+
# @api private
|
|
159
|
+
def perform_request(client, verb, uri, merged)
|
|
160
|
+
cookie_jar = CookieJar.new
|
|
161
|
+
builder = Request::Builder.new(merged)
|
|
162
|
+
req = builder.build(verb, uri)
|
|
163
|
+
client ||= client_for_origin(req.uri.origin)
|
|
164
|
+
load_cookies(cookie_jar, req)
|
|
165
|
+
res = client.perform(req, merged)
|
|
166
|
+
store_cookies(cookie_jar, res)
|
|
167
|
+
|
|
168
|
+
return res unless merged.follow
|
|
169
|
+
|
|
170
|
+
perform_redirects(cookie_jar, client, req, res, merged)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Follow redirects with cookie management
|
|
174
|
+
#
|
|
175
|
+
# For persistent sessions, each redirect hop may target a different
|
|
176
|
+
# origin. The session looks up (or creates) a pooled {Client} for
|
|
177
|
+
# the redirect target's origin, allowing cross-origin redirects
|
|
178
|
+
# without raising {StateError}.
|
|
179
|
+
#
|
|
180
|
+
# @param jar [HTTP::CookieJar] the cookie jar
|
|
181
|
+
# @param client [HTTP::Client] the client for the initial request
|
|
182
|
+
# @param req [HTTP::Request] the original request
|
|
183
|
+
# @param res [HTTP::Response] the initial redirect response
|
|
184
|
+
# @param opts [HTTP::Options] the merged options
|
|
185
|
+
# @return [HTTP::Response] the final non-redirect response
|
|
186
|
+
# @api private
|
|
187
|
+
def perform_redirects(jar, client, req, res, opts)
|
|
188
|
+
builder = Request::Builder.new(opts)
|
|
189
|
+
follow = opts.follow || {} #: Hash[untyped, untyped]
|
|
190
|
+
Redirector.new(**follow).perform(req, res) do |redirect_req|
|
|
191
|
+
wrapped = builder.wrap(redirect_req)
|
|
192
|
+
apply_cookies(jar, wrapped)
|
|
193
|
+
apply_cookies(jar, redirect_req)
|
|
194
|
+
response = redirect_client(client, wrapped).perform(wrapped, opts)
|
|
195
|
+
store_cookies(jar, response)
|
|
196
|
+
response
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Return the appropriate client for a redirect hop
|
|
201
|
+
#
|
|
202
|
+
# @param client [HTTP::Client] the client for the original request
|
|
203
|
+
# @param request [HTTP::Request] the redirect request
|
|
204
|
+
# @return [HTTP::Client] the client for the redirect target
|
|
205
|
+
# @api private
|
|
206
|
+
def redirect_client(client, request)
|
|
207
|
+
persistent? ? client_for_origin(request.uri.origin) : client
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Return a pooled persistent {Client} for the given origin
|
|
211
|
+
#
|
|
212
|
+
# Creates a new {Client} if one does not already exist for this origin.
|
|
213
|
+
# For the session's primary persistent origin, the default options are
|
|
214
|
+
# used directly. For other origins (e.g. redirect targets), the
|
|
215
|
+
# persistent origin is overridden and base_uri is cleared.
|
|
216
|
+
#
|
|
217
|
+
# @param origin [String] the URI origin (scheme + host + port)
|
|
218
|
+
# @return [HTTP::Client] a persistent client for the origin
|
|
219
|
+
# @api private
|
|
220
|
+
def client_for_origin(origin)
|
|
221
|
+
@clients[origin] ||= make_client(options_for_origin(origin))
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Build {Options} for a persistent client targeting the given origin
|
|
225
|
+
#
|
|
226
|
+
# @param origin [String] the URI origin
|
|
227
|
+
# @return [HTTP::Options] options configured for this origin
|
|
228
|
+
# @api private
|
|
229
|
+
def options_for_origin(origin)
|
|
230
|
+
return default_options if origin == default_options.persistent
|
|
231
|
+
|
|
232
|
+
default_options.merge(persistent: origin, base_uri: nil)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Load cookies from the request's Cookie header into the jar
|
|
236
|
+
#
|
|
237
|
+
# @param jar [HTTP::CookieJar] the cookie jar
|
|
238
|
+
# @param request [HTTP::Request] the request
|
|
239
|
+
# @return [void]
|
|
240
|
+
# @api private
|
|
241
|
+
def load_cookies(jar, request)
|
|
242
|
+
header = request.headers[Headers::COOKIE]
|
|
243
|
+
cookies = HTTP::Cookie.cookie_value_to_hash(header.to_s)
|
|
244
|
+
|
|
245
|
+
cookies.each do |name, value|
|
|
246
|
+
jar.add(HTTP::Cookie.new(name, value, path: request.uri.path, domain: request.host))
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Store cookies from the response's Set-Cookie headers into the jar
|
|
251
|
+
#
|
|
252
|
+
# @param jar [HTTP::CookieJar] the cookie jar
|
|
253
|
+
# @param response [HTTP::Response] the response
|
|
254
|
+
# @return [void]
|
|
255
|
+
# @api private
|
|
256
|
+
def store_cookies(jar, response)
|
|
257
|
+
response.cookies.each do |cookie|
|
|
258
|
+
if cookie.value == ""
|
|
259
|
+
jar.delete(cookie)
|
|
260
|
+
else
|
|
261
|
+
jar.add(cookie)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Apply cookies from the jar to the request's Cookie header
|
|
267
|
+
#
|
|
268
|
+
# @param jar [HTTP::CookieJar] the cookie jar
|
|
269
|
+
# @param request [HTTP::Request] the request
|
|
270
|
+
# @return [void]
|
|
271
|
+
# @api private
|
|
272
|
+
def apply_cookies(jar, request)
|
|
273
|
+
if jar.empty?
|
|
274
|
+
request.headers.delete(Headers::COOKIE)
|
|
275
|
+
else
|
|
276
|
+
request.headers.set(Headers::COOKIE, jar.map { |c| "#{c.name}=#{c.value}" }.join("; "))
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|