berkeley_library-util 0.1.7 → 0.1.9
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/.github/workflows/build.yml +1 -1
- data/CHANGES.md +9 -0
- data/lib/berkeley_library/util/module_info.rb +1 -1
- data/lib/berkeley_library/util/uris/exceptions.rb +15 -0
- data/lib/berkeley_library/util/uris/requester/class_methods.rb +89 -0
- data/lib/berkeley_library/util/uris/requester.rb +71 -68
- data/lib/berkeley_library/util/uris.rb +1 -1
- data/spec/berkeley_library/util/uris/requester_spec.rb +244 -0
- data/spec/berkeley_library/util/uris_spec.rb +8 -14
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fdc84acc6adb94fcfbbcf7d18b9a6bead8ef484ef40028f139b07964b565b4e2
|
4
|
+
data.tar.gz: 43e850e1ee541d777c07f546f789e576b45a44580734206c44cd2dce4d3293a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dc47ba39cee501522db01aceeba7060f51ee4270ca03a2d4263e836190eddc2e953fd83eb292379ce4fcdbd5a9bac3831ec76c750cb2ecd9a375c86a60533079
|
7
|
+
data.tar.gz: 0dc4190292083d68066528c6a7efa815667ccc6ba79fb75ba0d0fa5e5f1ab354465447d1d7b53bbf825426e010bd4954b0ada17f3b935ae68095d8d0795940c1
|
data/.github/workflows/build.yml
CHANGED
data/CHANGES.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
# 0.1.9 (2023-06-01)
|
2
|
+
|
3
|
+
- `URIs#path_escape` now attempts to convert non-UTF-8 strings to UTF-8 rather than immediately
|
4
|
+
raising an error.
|
5
|
+
|
6
|
+
# 0.1.8 (2023-03-20)
|
7
|
+
|
8
|
+
- Add `Retry-After` support to `Requester` for `429 Too Many Requests` and `503 Service Unavailable`.
|
9
|
+
|
1
10
|
# 0.1.7 (2023-03-15)
|
2
11
|
|
3
12
|
- Allow passing `log: false` to `Requester` methods (and corresponding `URIs` convenience
|
@@ -7,7 +7,7 @@ module BerkeleyLibrary
|
|
7
7
|
SUMMARY = 'Miscellaneous Ruby utilities for the UC Berkeley Library'.freeze
|
8
8
|
DESCRIPTION = 'A collection of miscellaneous Ruby routines for the UC Berkeley Library.'.freeze
|
9
9
|
LICENSE = 'MIT'.freeze
|
10
|
-
VERSION = '0.1.
|
10
|
+
VERSION = '0.1.9'.freeze
|
11
11
|
HOMEPAGE = 'https://github.com/BerkeleyLibrary/util'.freeze
|
12
12
|
end
|
13
13
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class RetryDelayTooLarge < RestClient::Exception
|
2
|
+
def initialize(response, delay:, max_delay:)
|
3
|
+
super(response, response.code)
|
4
|
+
|
5
|
+
self.message = 'Retry delay of %0.2gs exceeds limit of %0.2gs' % [delay, max_delay]
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class RetryLimitExceeded < RestClient::Exception
|
10
|
+
def initialize(response, max_retries:)
|
11
|
+
super(response, response.code)
|
12
|
+
|
13
|
+
self.message = "Retry limit (#{max_retries}) exceeded"
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module BerkeleyLibrary
|
2
|
+
module Util
|
3
|
+
module URIs
|
4
|
+
class Requester
|
5
|
+
# rubocop:disable Metrics/ParameterLists
|
6
|
+
module ClassMethods
|
7
|
+
# Performs a GET request and returns the response body as a string.
|
8
|
+
#
|
9
|
+
# @param uri [URI, String] the URI to GET
|
10
|
+
# @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
|
11
|
+
# @param headers [Hash] the request headers.
|
12
|
+
# @return [String] the body as a string.
|
13
|
+
# @param log [Boolean] whether to log each request URL and response code
|
14
|
+
# @param max_retries [Integer] the maximum number of times to retry after a 429 or 503 with Retry-After
|
15
|
+
# @param max_retry_delay [Integer] the maximum retry delay (in seconds) to accept in a Retry-After header
|
16
|
+
# @raise [RestClient::Exception] in the event of an unsuccessful request.
|
17
|
+
def get(uri, params: {}, headers: {}, log: true, max_retries: MAX_RETRIES, max_retry_delay: MAX_RETRY_DELAY_SECONDS)
|
18
|
+
resp = make_request(:get, uri, params, headers, log, max_retries, max_retry_delay)
|
19
|
+
resp.body
|
20
|
+
end
|
21
|
+
|
22
|
+
# Performs a HEAD request and returns the response status as an integer.
|
23
|
+
# Note that unlike {Requester#get}, this does not raise an error in the
|
24
|
+
# event of an unsuccessful request.
|
25
|
+
#
|
26
|
+
# @param uri [URI, String] the URI to HEAD
|
27
|
+
# @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
|
28
|
+
# @param headers [Hash] the request headers.
|
29
|
+
# @param log [Boolean] whether to log each request URL and response code
|
30
|
+
# @return [Integer] the response code as an integer.
|
31
|
+
def head(uri, params: {}, headers: {}, log: true, max_retries: MAX_RETRIES, max_retry_delay: MAX_RETRY_DELAY_SECONDS)
|
32
|
+
head_response(uri, params: params, headers: headers, log: log, max_retries: max_retries, max_retry_delay: max_retry_delay).code
|
33
|
+
end
|
34
|
+
|
35
|
+
# Performs a GET request and returns the response, even in the event of
|
36
|
+
# a failed request.
|
37
|
+
#
|
38
|
+
# @param uri [URI, String] the URI to GET
|
39
|
+
# @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
|
40
|
+
# @param headers [Hash] the request headers.
|
41
|
+
# @param log [Boolean] whether to log each request URL and response code
|
42
|
+
# @return [RestClient::Response] the response
|
43
|
+
def get_response(uri, params: {}, headers: {}, log: true, max_retries: MAX_RETRIES, max_retry_delay: MAX_RETRY_DELAY_SECONDS)
|
44
|
+
make_request(:get, uri, params, headers, log, max_retries, max_retry_delay)
|
45
|
+
rescue RestClient::Exception => e
|
46
|
+
e.response
|
47
|
+
end
|
48
|
+
|
49
|
+
# Performs a HEAD request and returns the response, even in the event of
|
50
|
+
# a failed request.
|
51
|
+
#
|
52
|
+
# @param uri [URI, String] the URI to HEAD
|
53
|
+
# @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
|
54
|
+
# @param headers [Hash] the request headers.
|
55
|
+
# @param log [Boolean] whether to log each request URL and response code
|
56
|
+
# @return [RestClient::Response] the response
|
57
|
+
def head_response(uri, params: {}, headers: {}, log: true, max_retries: MAX_RETRIES, max_retry_delay: MAX_RETRY_DELAY_SECONDS)
|
58
|
+
make_request(:head, uri, params, headers, log, max_retries, max_retry_delay)
|
59
|
+
rescue RestClient::Exception => e
|
60
|
+
e.response
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def make_request(method, url, params, headers, log, max_retries, max_retry_delay)
|
66
|
+
Requester.new(
|
67
|
+
method,
|
68
|
+
url,
|
69
|
+
params: params,
|
70
|
+
headers: headers,
|
71
|
+
log: log,
|
72
|
+
max_retries: max_retries,
|
73
|
+
max_retry_delay: max_retry_delay
|
74
|
+
).make_request
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
# rubocop:enable Metrics/ParameterLists
|
79
|
+
|
80
|
+
# ------------------------------------------------------------
|
81
|
+
# Class methods
|
82
|
+
|
83
|
+
class << self
|
84
|
+
include ClassMethods
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -1,6 +1,9 @@
|
|
1
|
+
require 'time'
|
1
2
|
require 'rest-client'
|
2
3
|
require 'berkeley_library/util/uris/appender'
|
4
|
+
require 'berkeley_library/util/uris/exceptions'
|
3
5
|
require 'berkeley_library/util/uris/validator'
|
6
|
+
require 'berkeley_library/util/uris/requester/class_methods'
|
4
7
|
require 'berkeley_library/logging'
|
5
8
|
|
6
9
|
module BerkeleyLibrary
|
@@ -9,80 +12,19 @@ module BerkeleyLibrary
|
|
9
12
|
class Requester
|
10
13
|
include BerkeleyLibrary::Logging
|
11
14
|
|
12
|
-
# ------------------------------------------------------------
|
13
|
-
# Class methods
|
14
|
-
|
15
|
-
class << self
|
16
|
-
# Performs a GET request and returns the response body as a string.
|
17
|
-
#
|
18
|
-
# @param uri [URI, String] the URI to GET
|
19
|
-
# @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
|
20
|
-
# @param headers [Hash] the request headers.
|
21
|
-
# @return [String] the body as a string.
|
22
|
-
# @param log [Boolean] whether to log each request URL and response code
|
23
|
-
# @raise [RestClient::Exception] in the event of an unsuccessful request.
|
24
|
-
def get(uri, params: {}, headers: {}, log: true)
|
25
|
-
resp = make_request(:get, uri, params, headers, log)
|
26
|
-
resp.body
|
27
|
-
end
|
28
|
-
|
29
|
-
# Performs a HEAD request and returns the response status as an integer.
|
30
|
-
# Note that unlike {Requester#get}, this does not raise an error in the
|
31
|
-
# event of an unsuccessful request.
|
32
|
-
#
|
33
|
-
# @param uri [URI, String] the URI to HEAD
|
34
|
-
# @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
|
35
|
-
# @param headers [Hash] the request headers.
|
36
|
-
# @param log [Boolean] whether to log each request URL and response code
|
37
|
-
# @return [Integer] the response code as an integer.
|
38
|
-
def head(uri, params: {}, headers: {}, log: true)
|
39
|
-
head_response(uri, params: params, headers: headers, log: log).code
|
40
|
-
end
|
41
|
-
|
42
|
-
# Performs a GET request and returns the response, even in the event of
|
43
|
-
# a failed request.
|
44
|
-
#
|
45
|
-
# @param uri [URI, String] the URI to GET
|
46
|
-
# @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
|
47
|
-
# @param headers [Hash] the request headers.
|
48
|
-
# @param log [Boolean] whether to log each request URL and response code
|
49
|
-
# @return [RestClient::Response] the body as a string.
|
50
|
-
def get_response(uri, params: {}, headers: {}, log: true)
|
51
|
-
make_request(:get, uri, params, headers, log)
|
52
|
-
rescue RestClient::Exception => e
|
53
|
-
e.response
|
54
|
-
end
|
55
|
-
|
56
|
-
# Performs a HEAD request and returns the response, even in the event of
|
57
|
-
# a failed request.
|
58
|
-
#
|
59
|
-
# @param uri [URI, String] the URI to HEAD
|
60
|
-
# @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
|
61
|
-
# @param headers [Hash] the request headers.
|
62
|
-
# @param log [Boolean] whether to log each request URL and response code
|
63
|
-
# @return [RestClient::Response] the response
|
64
|
-
def head_response(uri, params: {}, headers: {}, log: true)
|
65
|
-
make_request(:head, uri, params, headers, log)
|
66
|
-
rescue RestClient::Exception => e
|
67
|
-
e.response
|
68
|
-
end
|
69
|
-
|
70
|
-
private
|
71
|
-
|
72
|
-
def make_request(method, url, params, headers, log)
|
73
|
-
Requester.new(method, url, params: params, headers: headers, log: log).make_request
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
15
|
# ------------------------------------------------------------
|
78
16
|
# Constants
|
79
17
|
|
80
18
|
SUPPORTED_METHODS = %i[get head].freeze
|
19
|
+
RETRY_HEADER = :retry_after
|
20
|
+
RETRY_STATUSES = [429, 503].freeze
|
21
|
+
MAX_RETRY_DELAY_SECONDS = 10
|
22
|
+
MAX_RETRIES = 3
|
81
23
|
|
82
24
|
# ------------------------------------------------------------
|
83
25
|
# Attributes
|
84
26
|
|
85
|
-
attr_reader :method, :url_str, :headers, :log
|
27
|
+
attr_reader :method, :url_str, :headers, :log, :max_retries, :max_retry_delay
|
86
28
|
|
87
29
|
# ------------------------------------------------------------
|
88
30
|
# Initializer
|
@@ -94,8 +36,11 @@ module BerkeleyLibrary
|
|
94
36
|
# @param params [Hash] the query parameters to add to the URI. (Note that the URI may already include query parameters.)
|
95
37
|
# @param headers [Hash] the request headers.
|
96
38
|
# @param log [Boolean] whether to log each request URL and response code
|
39
|
+
# @param max_retries [Integer] the maximum number of times to retry after a 429 or 503 with Retry-After
|
40
|
+
# @param max_retry_delay [Integer] the maximum retry delay (in seconds) to accept in a Retry-After header
|
97
41
|
# @raise URI::InvalidURIError if the specified URL is invalid
|
98
|
-
|
42
|
+
# rubocop:disable Metrics/ParameterLists
|
43
|
+
def initialize(method, url, params: {}, headers: {}, log: true, max_retries: MAX_RETRIES, max_retry_delay: MAX_RETRY_DELAY_SECONDS)
|
99
44
|
raise ArgumentError, "#{method} not supported" unless SUPPORTED_METHODS.include?(method)
|
100
45
|
raise ArgumentError, 'url cannot be nil' unless (uri = Validator.uri_or_nil(url))
|
101
46
|
|
@@ -103,8 +48,12 @@ module BerkeleyLibrary
|
|
103
48
|
@url_str = url_str_with_params(uri, params)
|
104
49
|
@headers = headers
|
105
50
|
@log = log
|
51
|
+
@max_retries = max_retries
|
52
|
+
@max_retry_delay = max_retry_delay
|
106
53
|
end
|
107
54
|
|
55
|
+
# rubocop:enable Metrics/ParameterLists
|
56
|
+
|
108
57
|
# ------------------------------------------------------------
|
109
58
|
# Public instance methods
|
110
59
|
|
@@ -139,13 +88,30 @@ module BerkeleyLibrary
|
|
139
88
|
Appender.new(*elements).to_url_str
|
140
89
|
end
|
141
90
|
|
142
|
-
def execute_request
|
91
|
+
def execute_request(retries_remaining = max_retries)
|
92
|
+
try_execute_request
|
93
|
+
rescue RestClient::Exception => e
|
94
|
+
response = e.response
|
95
|
+
raise unless (retry_delay = retry_delay_from(response))
|
96
|
+
|
97
|
+
wait_for_retry(response, retry_delay, retries_remaining)
|
98
|
+
execute_request(retries_remaining - 1)
|
99
|
+
end
|
100
|
+
|
101
|
+
def try_execute_request
|
143
102
|
RestClient::Request.execute(method: method, url: url_str, headers: headers).tap do |response|
|
144
103
|
# Not all failed RestClient requests throw exceptions
|
145
104
|
raise(exception_for(response)) unless response.code == 200
|
146
105
|
end
|
147
106
|
end
|
148
107
|
|
108
|
+
def wait_for_retry(response, retry_delay, retries_remaining)
|
109
|
+
raise RetryLimitExceeded.new(response, max_retries: max_retries) unless retries_remaining > 0
|
110
|
+
raise RetryDelayTooLarge.new(response, delay: retry_delay, max_delay: max_retry_delay) if retry_delay > max_retry_delay
|
111
|
+
|
112
|
+
sleep(retry_delay)
|
113
|
+
end
|
114
|
+
|
149
115
|
def exception_for(resp)
|
150
116
|
status = resp.code
|
151
117
|
ex_class_for(status).new(resp, status).tap do |ex|
|
@@ -158,6 +124,43 @@ module BerkeleyLibrary
|
|
158
124
|
RestClient::Exceptions::EXCEPTIONS_MAP[status] || RestClient::RequestFailed
|
159
125
|
end
|
160
126
|
|
127
|
+
# Returns the retry interval for the specified exception, or `nil`
|
128
|
+
# if the response does not allow a retry.
|
129
|
+
#
|
130
|
+
# @param resp [RestClient::Response] the response
|
131
|
+
# @return [Integer, nil] the retry delay in seconds, or `nil` if the response
|
132
|
+
# does not allow a retry
|
133
|
+
def retry_delay_from(resp)
|
134
|
+
return unless RETRY_STATUSES.include?(resp.code)
|
135
|
+
return unless (retry_header_value = resp.headers[RETRY_HEADER])
|
136
|
+
return unless (retry_delay_seconds = parse_retry_header_value(retry_header_value))
|
137
|
+
|
138
|
+
[1, retry_delay_seconds.ceil].max
|
139
|
+
end
|
140
|
+
|
141
|
+
# @return [Float, nil] the retry delay in seconds, or `nil` if the delay cannot be parsed
|
142
|
+
def parse_retry_header_value(v)
|
143
|
+
# start by assuming it's a delay in seconds
|
144
|
+
Float(v) # should be an integer but let's not count on it
|
145
|
+
rescue ArgumentError
|
146
|
+
# assume it's an HTTP-date
|
147
|
+
parse_retry_after_date(v)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Parses the specified RFC2822 datetime string and returns the interval between that
|
151
|
+
# datetime and the current time in seconds
|
152
|
+
#
|
153
|
+
# @param date_str [String] an RFC2822 datetime string
|
154
|
+
# @return [Float, nil] the interval between the current time and the specified datetime,
|
155
|
+
# or nil if `date_str` cannot be parsed
|
156
|
+
def parse_retry_after_date(date_str)
|
157
|
+
retry_after = DateTime.rfc2822(date_str).to_time
|
158
|
+
retry_after - Time.now
|
159
|
+
rescue ArgumentError
|
160
|
+
logger.warn("Can't parse #{RETRY_HEADER} value #{date_str}")
|
161
|
+
nil
|
162
|
+
end
|
163
|
+
|
161
164
|
end
|
162
165
|
end
|
163
166
|
end
|
@@ -88,8 +88,8 @@ module BerkeleyLibrary
|
|
88
88
|
# replacing disallowed characters (including /) with percent-encodings as needed.
|
89
89
|
def path_escape(s)
|
90
90
|
raise ArgumentError, "Can't escape #{s.inspect}: not a string" unless s.respond_to?(:encoding)
|
91
|
-
raise ArgumentError, "Can't escape #{s.inspect}: expected #{UTF_8}, was #{s.encoding}" unless s.encoding == UTF_8
|
92
91
|
|
92
|
+
s = s.encode(UTF_8) unless s.encoding == UTF_8
|
93
93
|
''.tap do |escaped|
|
94
94
|
s.bytes.each do |b|
|
95
95
|
escaped << (should_escape?(b, :path_segment) ? '%%%02X' % b : b.chr)
|
@@ -65,6 +65,250 @@ module BerkeleyLibrary
|
|
65
65
|
result = Requester.get(url1)
|
66
66
|
expect(result).to eq(expected_body)
|
67
67
|
end
|
68
|
+
|
69
|
+
describe 'retries' do
|
70
|
+
let(:url) { 'https://example.org/' }
|
71
|
+
let(:expected_body) { 'Help! I am trapped in a unit test' }
|
72
|
+
|
73
|
+
context 'handling 429 Too Many Requests' do
|
74
|
+
context 'with Retry-After' do
|
75
|
+
context 'in seconds' do
|
76
|
+
it 'retries after the specified delay' do
|
77
|
+
retry_after_seconds = 1
|
78
|
+
|
79
|
+
stub_request(:get, url)
|
80
|
+
.to_return(status: 429, headers: { 'Retry-After' => retry_after_seconds.to_s })
|
81
|
+
.to_return(status: 200, body: expected_body)
|
82
|
+
|
83
|
+
requester = Requester.new(:get, url)
|
84
|
+
expect(requester).to receive(:sleep).with(1).once
|
85
|
+
|
86
|
+
result = requester.make_request
|
87
|
+
expect(result).to eq(expected_body)
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'handles a non-integer retry delay' do
|
91
|
+
retry_after_seconds = 1.5
|
92
|
+
stub_request(:get, url)
|
93
|
+
.to_return(status: 429, headers: { 'Retry-After' => retry_after_seconds.to_s })
|
94
|
+
.to_return(status: 200, body: expected_body)
|
95
|
+
|
96
|
+
requester = Requester.new(:get, url)
|
97
|
+
expect(requester).to receive(:sleep).with(2).once
|
98
|
+
|
99
|
+
result = requester.make_request
|
100
|
+
expect(result).to eq(expected_body)
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'raises RetryDelayTooLarge if the delay is too large' do
|
104
|
+
retry_after_seconds = 10 + BerkeleyLibrary::Util::URIs::Requester::MAX_RETRY_DELAY_SECONDS
|
105
|
+
|
106
|
+
stub_request(:get, url)
|
107
|
+
.to_return(status: 429, headers: { 'Retry-After' => retry_after_seconds.to_s })
|
108
|
+
|
109
|
+
requester = Requester.new(:get, url)
|
110
|
+
expect(requester).not_to receive(:sleep)
|
111
|
+
|
112
|
+
expect { requester.make_request }.to raise_error(RetryDelayTooLarge) do |ex|
|
113
|
+
expect(ex.cause).to be_a(RestClient::TooManyRequests)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'raises RetryLimitExceeded if there are too many retries' do
|
118
|
+
retry_after_seconds = 1
|
119
|
+
|
120
|
+
stub_request(:get, url)
|
121
|
+
.to_return(status: 429, headers: { 'Retry-After' => retry_after_seconds.to_s })
|
122
|
+
.to_return(status: 429, headers: { 'Retry-After' => retry_after_seconds.to_s })
|
123
|
+
|
124
|
+
requester = Requester.new(:get, url, max_retries: 1)
|
125
|
+
expect(requester).to receive(:sleep).with(1).once
|
126
|
+
|
127
|
+
expect { requester.make_request }.to raise_error(RetryLimitExceeded) do |ex|
|
128
|
+
expect(ex.cause).to be_a(RestClient::TooManyRequests)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context 'as RFC2822 datetime' do
|
134
|
+
it 'retries after the specified delay' do
|
135
|
+
retry_after_seconds = 1
|
136
|
+
retry_after_datetime = (Time.now + retry_after_seconds)
|
137
|
+
|
138
|
+
stub_request(:get, url)
|
139
|
+
.to_return(status: 429, headers: { 'Retry-After' => retry_after_datetime.rfc2822 })
|
140
|
+
.to_return(status: 200, body: expected_body)
|
141
|
+
|
142
|
+
requester = Requester.new(:get, url)
|
143
|
+
expect(requester).to receive(:sleep).with(1).once
|
144
|
+
|
145
|
+
result = requester.make_request
|
146
|
+
expect(result).to eq(expected_body)
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'handles a non-integer retry delay' do
|
150
|
+
retry_after_seconds = 2.75
|
151
|
+
retry_after_datetime = (Time.now + retry_after_seconds)
|
152
|
+
|
153
|
+
stub_request(:get, url)
|
154
|
+
.to_return(status: 429, headers: { 'Retry-After' => retry_after_datetime.rfc2822 })
|
155
|
+
.to_return(status: 200, body: expected_body)
|
156
|
+
|
157
|
+
requester = Requester.new(:get, url)
|
158
|
+
expected_value = a_value_within(1).of(retry_after_seconds)
|
159
|
+
expect(requester).to receive(:sleep).with(expected_value).once
|
160
|
+
|
161
|
+
result = requester.make_request
|
162
|
+
expect(result).to eq(expected_body)
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'raises RetryDelayTooLarge if the delay is too large' do
|
166
|
+
retry_after_seconds = 10 + BerkeleyLibrary::Util::URIs::Requester::MAX_RETRY_DELAY_SECONDS
|
167
|
+
retry_after_datetime = (Time.now + retry_after_seconds)
|
168
|
+
|
169
|
+
stub_request(:get, url)
|
170
|
+
.to_return(status: 429, headers: { 'Retry-After' => retry_after_datetime.rfc2822 })
|
171
|
+
|
172
|
+
requester = Requester.new(:get, url)
|
173
|
+
expect(requester).not_to receive(:sleep)
|
174
|
+
|
175
|
+
expect { requester.make_request }.to raise_error(RetryDelayTooLarge) do |ex|
|
176
|
+
expect(ex.cause).to be_a(RestClient::TooManyRequests)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'raises RetryLimitExceeded if there are too many retries' do
|
181
|
+
retry_after_seconds = 1
|
182
|
+
retry_after_datetime = (Time.now + retry_after_seconds)
|
183
|
+
|
184
|
+
stub_request(:get, url)
|
185
|
+
.to_return(status: 429, headers: { 'Retry-After' => retry_after_datetime.rfc2822 })
|
186
|
+
.to_return(status: 429, headers: { 'Retry-After' => retry_after_datetime.rfc2822 })
|
187
|
+
|
188
|
+
requester = Requester.new(:get, url, max_retries: 1)
|
189
|
+
expect(requester).to receive(:sleep).with(1).once
|
190
|
+
|
191
|
+
expect { requester.make_request }.to raise_error(RetryLimitExceeded) do |ex|
|
192
|
+
expect(ex.cause).to be_a(RestClient::TooManyRequests)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'ignores an invalid Retry-After' do
|
199
|
+
stub_request(:get, url)
|
200
|
+
.to_return(status: 429, headers: { 'Retry-After' => 'the end of the world' })
|
201
|
+
|
202
|
+
requester = Requester.new(:get, url)
|
203
|
+
expect { requester.make_request }.to raise_error(RestClient::TooManyRequests)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
context 'handling 503 Service Unavailable' do
|
209
|
+
context 'with Retry-After' do
|
210
|
+
context 'in seconds' do
|
211
|
+
it 'retries after the specified delay' do
|
212
|
+
retry_after_seconds = 1
|
213
|
+
|
214
|
+
stub_request(:get, url)
|
215
|
+
.to_return(status: 503, headers: { 'Retry-After' => retry_after_seconds.to_s })
|
216
|
+
.to_return(status: 200, body: expected_body)
|
217
|
+
|
218
|
+
requester = Requester.new(:get, url)
|
219
|
+
expect(requester).to receive(:sleep).with(1).once
|
220
|
+
|
221
|
+
result = requester.make_request
|
222
|
+
expect(result).to eq(expected_body)
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'handles a non-integer retry delay' do
|
226
|
+
retry_after_seconds = 0.75
|
227
|
+
stub_request(:get, url)
|
228
|
+
.to_return(status: 503, headers: { 'Retry-After' => retry_after_seconds.to_s })
|
229
|
+
.to_return(status: 200, body: expected_body)
|
230
|
+
|
231
|
+
requester = Requester.new(:get, url)
|
232
|
+
expect(requester).to receive(:sleep).with(1).once
|
233
|
+
|
234
|
+
result = requester.make_request
|
235
|
+
expect(result).to eq(expected_body)
|
236
|
+
end
|
237
|
+
|
238
|
+
it 'raises RetryDelayTooLarge if the delay is too large' do
|
239
|
+
retry_after_seconds = 10 + BerkeleyLibrary::Util::URIs::Requester::MAX_RETRY_DELAY_SECONDS
|
240
|
+
|
241
|
+
stub_request(:get, url)
|
242
|
+
.to_return(status: 503, headers: { 'Retry-After' => retry_after_seconds.to_s })
|
243
|
+
|
244
|
+
requester = Requester.new(:get, url)
|
245
|
+
expect(requester).not_to receive(:sleep)
|
246
|
+
|
247
|
+
expect { requester.make_request }.to raise_error(RetryDelayTooLarge) do |ex|
|
248
|
+
expect(ex.cause).to be_a(RestClient::ServiceUnavailable)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
context 'as RFC2822 datetime' do
|
254
|
+
it 'retries after the specified delay' do
|
255
|
+
retry_after_seconds = 1
|
256
|
+
retry_after_datetime = (Time.now + retry_after_seconds)
|
257
|
+
|
258
|
+
stub_request(:get, url)
|
259
|
+
.to_return(status: 503, headers: { 'Retry-After' => retry_after_datetime.rfc2822 })
|
260
|
+
.to_return(status: 200, body: expected_body)
|
261
|
+
|
262
|
+
requester = Requester.new(:get, url)
|
263
|
+
expect(requester).to receive(:sleep).with(1).once
|
264
|
+
|
265
|
+
result = requester.make_request
|
266
|
+
expect(result).to eq(expected_body)
|
267
|
+
end
|
268
|
+
|
269
|
+
it 'handles a non-integer retry delay' do
|
270
|
+
retry_after_seconds = 1.75
|
271
|
+
retry_after_datetime = (Time.now + retry_after_seconds)
|
272
|
+
|
273
|
+
stub_request(:get, url)
|
274
|
+
.to_return(status: 503, headers: { 'Retry-After' => retry_after_datetime.rfc2822 })
|
275
|
+
.to_return(status: 200, body: expected_body)
|
276
|
+
|
277
|
+
requester = Requester.new(:get, url)
|
278
|
+
expected_value = a_value_within(1).of(retry_after_seconds)
|
279
|
+
expect(requester).to receive(:sleep).with(expected_value).once
|
280
|
+
|
281
|
+
result = requester.make_request
|
282
|
+
expect(result).to eq(expected_body)
|
283
|
+
end
|
284
|
+
|
285
|
+
it 'raises RetryDelayTooLarge if the delay is too large' do
|
286
|
+
retry_after_seconds = 10 + BerkeleyLibrary::Util::URIs::Requester::MAX_RETRY_DELAY_SECONDS
|
287
|
+
retry_after_datetime = (Time.now + retry_after_seconds)
|
288
|
+
|
289
|
+
stub_request(:get, url)
|
290
|
+
.to_return(status: 503, headers: { 'Retry-After' => retry_after_datetime.rfc2822 })
|
291
|
+
|
292
|
+
requester = Requester.new(:get, url)
|
293
|
+
expect(requester).not_to receive(:sleep)
|
294
|
+
|
295
|
+
expect { requester.make_request }.to raise_error(RetryDelayTooLarge) do |ex|
|
296
|
+
expect(ex.cause).to be_a(RestClient::ServiceUnavailable)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
it 'ignores an invalid Retry-After' do
|
302
|
+
stub_request(:get, url)
|
303
|
+
.to_return(status: 503, headers: { 'Retry-After' => 'the end of the world' })
|
304
|
+
|
305
|
+
requester = Requester.new(:get, url)
|
306
|
+
expect { requester.make_request }.to raise_error(RestClient::ServiceUnavailable)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
68
312
|
end
|
69
313
|
|
70
314
|
describe :head do
|
@@ -337,20 +337,14 @@ module BerkeleyLibrary::Util
|
|
337
337
|
expect { URIs.path_escape(str.bytes) }.to raise_error(ArgumentError)
|
338
338
|
end
|
339
339
|
|
340
|
-
it '
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
# OK, we're really just testing String#encode here, but
|
350
|
-
# it's useful for documentation
|
351
|
-
in_str_sjis = in_str.encode(Encoding::Shift_JIS)
|
352
|
-
in_str_utf8 = in_str_sjis.encode(Encoding::UTF_8)
|
353
|
-
expect(URIs.path_escape(in_str_utf8)).to eq(out_str)
|
340
|
+
it 'converts non-UTF-8 strings to UTF-8' do
|
341
|
+
utf_16_be = Encoding.find('UTF-16BE')
|
342
|
+
aggregate_failures do
|
343
|
+
in_out.each do |in_str, out_str|
|
344
|
+
encoded = in_str.encode(utf_16_be)
|
345
|
+
expect(URIs.path_escape(encoded)).to eq(out_str)
|
346
|
+
end
|
347
|
+
end
|
354
348
|
end
|
355
349
|
end
|
356
350
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: berkeley_library-util
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- David Moles
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-06-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: berkeley_library-logging
|
@@ -241,7 +241,9 @@ files:
|
|
241
241
|
- lib/berkeley_library/util/times.rb
|
242
242
|
- lib/berkeley_library/util/uris.rb
|
243
243
|
- lib/berkeley_library/util/uris/appender.rb
|
244
|
+
- lib/berkeley_library/util/uris/exceptions.rb
|
244
245
|
- lib/berkeley_library/util/uris/requester.rb
|
246
|
+
- lib/berkeley_library/util/uris/requester/class_methods.rb
|
245
247
|
- lib/berkeley_library/util/uris/validator.rb
|
246
248
|
- rakelib/.rubocop.yml
|
247
249
|
- rakelib/coverage.rake
|