http 5.1.1 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +6 -24
- data/.rubocop/metrics.yml +4 -0
- data/.rubocop/rspec.yml +9 -0
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +45 -32
- data/CHANGELOG.md +57 -0
- data/{CHANGES.md → CHANGES_OLD.md} +1 -1
- data/Gemfile +1 -0
- data/README.md +4 -2
- data/SECURITY.md +13 -1
- data/http.gemspec +8 -2
- data/lib/http/base64.rb +12 -0
- data/lib/http/chainable.rb +27 -3
- data/lib/http/client.rb +1 -1
- data/lib/http/connection.rb +12 -3
- data/lib/http/errors.rb +16 -0
- data/lib/http/feature.rb +2 -1
- data/lib/http/features/instrumentation.rb +6 -1
- data/lib/http/features/raise_error.rb +22 -0
- data/lib/http/headers/normalizer.rb +69 -0
- data/lib/http/headers.rb +26 -40
- data/lib/http/request/writer.rb +2 -1
- data/lib/http/request.rb +15 -5
- 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/null.rb +8 -5
- data/lib/http/uri.rb +18 -2
- data/lib/http/version.rb +1 -1
- data/lib/http.rb +1 -0
- data/spec/lib/http/client_spec.rb +1 -0
- data/spec/lib/http/connection_spec.rb +23 -1
- data/spec/lib/http/features/instrumentation_spec.rb +19 -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/options/headers_spec.rb +5 -1
- data/spec/lib/http/redirector_spec.rb +6 -5
- 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_spec.rb +49 -3
- data/spec/spec_helper.rb +1 -0
- data/spec/support/dummy_server/servlet.rb +19 -6
- data/spec/support/dummy_server.rb +2 -1
- metadata +28 -8
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Headers::Normalizer do
|
4
|
+
subject(:normalizer) { described_class.new }
|
5
|
+
|
6
|
+
include_context RSpec::Memory
|
7
|
+
|
8
|
+
describe "#call" do
|
9
|
+
it "normalizes the header" do
|
10
|
+
expect(normalizer.call("content_type")).to eq "Content-Type"
|
11
|
+
end
|
12
|
+
|
13
|
+
it "returns a non-frozen string" do
|
14
|
+
expect(normalizer.call("content_type")).not_to be_frozen
|
15
|
+
end
|
16
|
+
|
17
|
+
it "evicts the oldest item when cache is full" do
|
18
|
+
max_headers = (1..described_class::Cache::MAX_SIZE).map { |i| "Header#{i}" }
|
19
|
+
max_headers.each { |header| normalizer.call(header) }
|
20
|
+
normalizer.call("New-Header")
|
21
|
+
cache_store = normalizer.instance_variable_get(:@cache).instance_variable_get(:@store)
|
22
|
+
expect(cache_store.keys).to eq(max_headers[1..] + ["New-Header"])
|
23
|
+
end
|
24
|
+
|
25
|
+
it "retuns mutable strings" do
|
26
|
+
normalized_headers = Array.new(3) { normalizer.call("content_type") }
|
27
|
+
|
28
|
+
expect(normalized_headers)
|
29
|
+
.to satisfy { |arr| arr.uniq.size == 1 }
|
30
|
+
.and(satisfy { |arr| arr.map(&:object_id).uniq.size == normalized_headers.size })
|
31
|
+
.and(satisfy { |arr| arr.none?(&:frozen?) })
|
32
|
+
end
|
33
|
+
|
34
|
+
it "allocates minimal memory for normalization of the same header" do
|
35
|
+
normalizer.call("accept") # XXX: Ensure normalizer is pre-allocated
|
36
|
+
|
37
|
+
# On first call it is expected to allocate during normalization
|
38
|
+
expect { normalizer.call("content_type") }.to limit_allocations(
|
39
|
+
Array => 1,
|
40
|
+
MatchData => 1,
|
41
|
+
String => 6
|
42
|
+
)
|
43
|
+
|
44
|
+
# On subsequent call it is expected to only allocate copy of a cached string
|
45
|
+
expect { normalizer.call("content_type") }.to limit_allocations(
|
46
|
+
Array => 0,
|
47
|
+
MatchData => 0,
|
48
|
+
String => 1
|
49
|
+
)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -14,7 +14,11 @@ RSpec.describe HTTP::Options, "headers" do
|
|
14
14
|
end
|
15
15
|
|
16
16
|
it "accepts any object that respond to :to_hash" do
|
17
|
-
x =
|
17
|
+
x = if RUBY_VERSION >= "3.2.0"
|
18
|
+
Data.define(:to_hash).new(:to_hash => { "accept" => "json" })
|
19
|
+
else
|
20
|
+
Struct.new(:to_hash).new({ "accept" => "json" })
|
21
|
+
end
|
18
22
|
expect(opts.with_headers(x).headers["accept"]).to eq("json")
|
19
23
|
end
|
20
24
|
end
|
@@ -115,12 +115,13 @@ RSpec.describe HTTP::Redirector do
|
|
115
115
|
expect(req_cookie).to eq request_cookies.shift
|
116
116
|
hops.shift
|
117
117
|
end
|
118
|
+
|
118
119
|
expect(res.to_s).to eq "bar"
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
120
|
+
expect(res.cookies.cookies.to_h { |c| [c.name, c.value] }).to eq({
|
121
|
+
"foo" => "42",
|
122
|
+
"bar" => "53",
|
123
|
+
"baz" => "65"
|
124
|
+
})
|
124
125
|
end
|
125
126
|
|
126
127
|
it "returns original cookies in response" do
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Retriable::DelayCalculator do
|
4
|
+
let(:response) do
|
5
|
+
HTTP::Response.new(
|
6
|
+
status: 200,
|
7
|
+
version: "1.1",
|
8
|
+
headers: {},
|
9
|
+
body: "Hello world!",
|
10
|
+
request: HTTP::Request.new(verb: :get, uri: "http://example.com")
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
def call_delay(iterations, **options)
|
15
|
+
described_class.new(options).call(iterations, response)
|
16
|
+
end
|
17
|
+
|
18
|
+
def call_retry_header(value, **options)
|
19
|
+
response.headers["Retry-After"] = value
|
20
|
+
described_class.new(options).call(rand(1...100), response)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "prevents negative sleep time" do
|
24
|
+
expect(call_delay(20, delay: -20)).to eq 0
|
25
|
+
end
|
26
|
+
|
27
|
+
it "backs off exponentially" do
|
28
|
+
expect(call_delay(1)).to be_between 0, 1
|
29
|
+
expect(call_delay(2)).to be_between 1, 2
|
30
|
+
expect(call_delay(3)).to be_between 3, 4
|
31
|
+
expect(call_delay(4)).to be_between 7, 8
|
32
|
+
expect(call_delay(5)).to be_between 15, 16
|
33
|
+
end
|
34
|
+
|
35
|
+
it "can have a maximum wait time" do
|
36
|
+
expect(call_delay(1, max_delay: 5)).to be_between 0, 1
|
37
|
+
expect(call_delay(5, max_delay: 5)).to eq 5
|
38
|
+
end
|
39
|
+
|
40
|
+
it "respects Retry-After headers as integer" do
|
41
|
+
delay_time = rand(6...2500)
|
42
|
+
header_value = delay_time.to_s
|
43
|
+
expect(call_retry_header(header_value)).to eq delay_time
|
44
|
+
expect(call_retry_header(header_value, max_delay: 5)).to eq 5
|
45
|
+
end
|
46
|
+
|
47
|
+
it "respects Retry-After headers as rfc2822 timestamp" do
|
48
|
+
delay_time = rand(6...2500)
|
49
|
+
header_value = (Time.now.gmtime + delay_time).to_datetime.rfc2822.sub("+0000", "GMT")
|
50
|
+
expect(call_retry_header(header_value)).to be_within(1).of(delay_time)
|
51
|
+
expect(call_retry_header(header_value, max_delay: 5)).to eq 5
|
52
|
+
end
|
53
|
+
|
54
|
+
it "respects Retry-After headers as rfc2822 timestamp in the past" do
|
55
|
+
delay_time = rand(6...2500)
|
56
|
+
header_value = (Time.now.gmtime - delay_time).to_datetime.rfc2822.sub("+0000", "GMT")
|
57
|
+
expect(call_retry_header(header_value)).to eq 0
|
58
|
+
end
|
59
|
+
|
60
|
+
it "does not error on invalid Retry-After header" do
|
61
|
+
[ # invalid strings
|
62
|
+
"This is a string with a number 5 in it",
|
63
|
+
"8 Eight is the first digit in this string",
|
64
|
+
"This is a string with a #{Time.now.gmtime.to_datetime.rfc2822} timestamp in it"
|
65
|
+
].each do |header_value|
|
66
|
+
expect(call_retry_header(header_value)).to eq 0
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,302 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Retriable::Performer do
|
4
|
+
let(:client) do
|
5
|
+
HTTP::Client.new
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:response) do
|
9
|
+
HTTP::Response.new(
|
10
|
+
status: 200,
|
11
|
+
version: "1.1",
|
12
|
+
headers: {},
|
13
|
+
body: "Hello world!",
|
14
|
+
request: request
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
let(:request) do
|
19
|
+
HTTP::Request.new(
|
20
|
+
verb: :get,
|
21
|
+
uri: "http://example.com"
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:perform_spy) { { counter: 0 } }
|
26
|
+
let(:counter_spy) { perform_spy[:counter] }
|
27
|
+
|
28
|
+
before do
|
29
|
+
stub_const("CustomException", Class.new(StandardError))
|
30
|
+
end
|
31
|
+
|
32
|
+
def perform(options = {}, client_arg = client, request_arg = request, &block)
|
33
|
+
# by explicitly overwriting the default delay, we make a much faster test suite
|
34
|
+
default_options = { delay: 0 }
|
35
|
+
options = default_options.merge(options)
|
36
|
+
|
37
|
+
HTTP::Retriable::Performer
|
38
|
+
.new(options)
|
39
|
+
.perform(client_arg, request_arg) do
|
40
|
+
perform_spy[:counter] += 1
|
41
|
+
block ? yield : response
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def measure_wait
|
46
|
+
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
47
|
+
result = yield
|
48
|
+
t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
49
|
+
[t2 - t1, result]
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#perform" do
|
53
|
+
describe "expected exception" do
|
54
|
+
it "retries the request" do
|
55
|
+
expect do
|
56
|
+
perform(exceptions: [CustomException], tries: 2) do
|
57
|
+
raise CustomException
|
58
|
+
end
|
59
|
+
end.to raise_error HTTP::OutOfRetriesError
|
60
|
+
|
61
|
+
expect(counter_spy).to eq 2
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "unexpected exception" do
|
66
|
+
it "does not retry the request" do
|
67
|
+
expect do
|
68
|
+
perform(exceptions: [], tries: 2) do
|
69
|
+
raise CustomException
|
70
|
+
end
|
71
|
+
end.to raise_error CustomException
|
72
|
+
|
73
|
+
expect(counter_spy).to eq 1
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "expected status codes" do
|
78
|
+
def response(**options)
|
79
|
+
HTTP::Response.new(
|
80
|
+
{
|
81
|
+
status: 200,
|
82
|
+
version: "1.1",
|
83
|
+
headers: {},
|
84
|
+
body: "Hello world!",
|
85
|
+
request: request
|
86
|
+
}.merge(options)
|
87
|
+
)
|
88
|
+
end
|
89
|
+
|
90
|
+
it "retries the request" do
|
91
|
+
expect do
|
92
|
+
perform(retry_statuses: [200], tries: 2)
|
93
|
+
end.to raise_error HTTP::OutOfRetriesError
|
94
|
+
|
95
|
+
expect(counter_spy).to eq 2
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "status codes can be expressed in many ways" do
|
99
|
+
[
|
100
|
+
301,
|
101
|
+
[200, 301, 485],
|
102
|
+
250...400,
|
103
|
+
[250...Float::INFINITY],
|
104
|
+
->(status_code) { status_code == 301 },
|
105
|
+
[->(status_code) { status_code == 301 }]
|
106
|
+
].each do |retry_statuses|
|
107
|
+
it retry_statuses.to_s do
|
108
|
+
expect do
|
109
|
+
perform(retry_statuses: retry_statuses, tries: 2) do
|
110
|
+
response(status: 301)
|
111
|
+
end
|
112
|
+
end.to raise_error HTTP::OutOfRetriesError
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "unexpected status code" do
|
119
|
+
it "does not retry the request" do
|
120
|
+
expect(
|
121
|
+
perform(retry_statuses: [], tries: 2)
|
122
|
+
).to eq response
|
123
|
+
|
124
|
+
expect(counter_spy).to eq 1
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe "on_retry callback" do
|
129
|
+
it "calls the on_retry callback on each retry with exception" do
|
130
|
+
callback_call_spy = 0
|
131
|
+
|
132
|
+
callback_spy = proc do |callback_request, error, callback_response|
|
133
|
+
expect(callback_request).to eq request
|
134
|
+
expect(error).to be_a HTTP::TimeoutError
|
135
|
+
expect(callback_response).to be_nil
|
136
|
+
callback_call_spy += 1
|
137
|
+
end
|
138
|
+
|
139
|
+
expect do
|
140
|
+
perform(tries: 3, on_retry: callback_spy) do
|
141
|
+
raise HTTP::TimeoutError
|
142
|
+
end
|
143
|
+
end.to raise_error HTTP::OutOfRetriesError
|
144
|
+
|
145
|
+
expect(callback_call_spy).to eq 2
|
146
|
+
end
|
147
|
+
|
148
|
+
it "calls the on_retry callback on each retry with response" do
|
149
|
+
callback_call_spy = 0
|
150
|
+
|
151
|
+
callback_spy = proc do |callback_request, error, callback_response|
|
152
|
+
expect(callback_request).to eq request
|
153
|
+
expect(error).to be_nil
|
154
|
+
expect(callback_response).to be response
|
155
|
+
callback_call_spy += 1
|
156
|
+
end
|
157
|
+
|
158
|
+
expect do
|
159
|
+
perform(retry_statuses: [200], tries: 3, on_retry: callback_spy)
|
160
|
+
end.to raise_error HTTP::OutOfRetriesError
|
161
|
+
|
162
|
+
expect(callback_call_spy).to eq 2
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
describe "delay option" do
|
167
|
+
let(:timing_slack) { 0.05 }
|
168
|
+
|
169
|
+
it "can be a positive number" do
|
170
|
+
time, = measure_wait do
|
171
|
+
perform(delay: 0.1, tries: 3, should_retry: ->(*) { true })
|
172
|
+
rescue HTTP::OutOfRetriesError
|
173
|
+
end
|
174
|
+
expect(time).to be_within(timing_slack).of(0.2)
|
175
|
+
end
|
176
|
+
|
177
|
+
it "can be a proc number" do
|
178
|
+
time, = measure_wait do
|
179
|
+
perform(delay: ->(attempt) { attempt / 10.0 }, tries: 3, should_retry: ->(*) { true })
|
180
|
+
rescue HTTP::OutOfRetriesError
|
181
|
+
end
|
182
|
+
expect(time).to be_within(timing_slack).of(0.3)
|
183
|
+
end
|
184
|
+
|
185
|
+
it "receives correct retry number when a proc" do
|
186
|
+
retry_count = 0
|
187
|
+
retry_proc = proc do |attempt|
|
188
|
+
expect(attempt).to eq(retry_count).and(be > 0)
|
189
|
+
0
|
190
|
+
end
|
191
|
+
begin
|
192
|
+
perform(delay: retry_proc, should_retry: ->(*) { true }) do
|
193
|
+
retry_count += 1
|
194
|
+
response
|
195
|
+
end
|
196
|
+
rescue HTTP::OutOfRetriesError
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
describe "should_retry option" do
|
202
|
+
it "decides if the request should be retried" do # rubocop:disable RSpec/MultipleExpectations
|
203
|
+
retry_proc = proc do |req, err, res, attempt|
|
204
|
+
expect(req).to eq request
|
205
|
+
if res
|
206
|
+
expect(err).to be_nil
|
207
|
+
expect(res).to be response
|
208
|
+
else
|
209
|
+
expect(err).to be_a CustomException
|
210
|
+
expect(res).to be_nil
|
211
|
+
end
|
212
|
+
|
213
|
+
attempt < 5
|
214
|
+
end
|
215
|
+
|
216
|
+
begin
|
217
|
+
perform(should_retry: retry_proc) do
|
218
|
+
rand < 0.5 ? response : raise(CustomException)
|
219
|
+
end
|
220
|
+
rescue CustomException
|
221
|
+
end
|
222
|
+
|
223
|
+
expect(counter_spy).to eq 5
|
224
|
+
end
|
225
|
+
|
226
|
+
it "raises the original error if not retryable" do
|
227
|
+
retry_proc = ->(*) { false }
|
228
|
+
|
229
|
+
expect do
|
230
|
+
perform(should_retry: retry_proc) do
|
231
|
+
raise CustomException
|
232
|
+
end
|
233
|
+
end.to raise_error CustomException
|
234
|
+
|
235
|
+
expect(counter_spy).to eq 1
|
236
|
+
end
|
237
|
+
|
238
|
+
it "raises HTTP::OutOfRetriesError if retryable" do
|
239
|
+
retry_proc = ->(*) { true }
|
240
|
+
|
241
|
+
expect do
|
242
|
+
perform(should_retry: retry_proc) do
|
243
|
+
raise CustomException
|
244
|
+
end
|
245
|
+
end.to raise_error HTTP::OutOfRetriesError
|
246
|
+
|
247
|
+
expect(counter_spy).to eq 5
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe "connection closing" do
|
253
|
+
let(:client) { double(:client) }
|
254
|
+
|
255
|
+
it "does not close the connection if we get a propper response" do
|
256
|
+
expect(client).not_to receive(:close)
|
257
|
+
perform
|
258
|
+
end
|
259
|
+
|
260
|
+
it "closes the connection after each raiseed attempt" do
|
261
|
+
expect(client).to receive(:close).exactly(3).times
|
262
|
+
begin
|
263
|
+
perform(should_retry: ->(*) { true }, tries: 3)
|
264
|
+
rescue HTTP::OutOfRetriesError
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
it "closes the connection on an unexpected exception" do
|
269
|
+
expect(client).to receive(:close)
|
270
|
+
begin
|
271
|
+
perform do
|
272
|
+
raise CustomException
|
273
|
+
end
|
274
|
+
rescue CustomException
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
describe HTTP::OutOfRetriesError do
|
280
|
+
it "has the original exception as a cause if available" do
|
281
|
+
err = nil
|
282
|
+
begin
|
283
|
+
perform(exceptions: [CustomException]) do
|
284
|
+
raise CustomException
|
285
|
+
end
|
286
|
+
rescue described_class => e
|
287
|
+
err = e
|
288
|
+
end
|
289
|
+
expect(err.cause).to be_a CustomException
|
290
|
+
end
|
291
|
+
|
292
|
+
it "has the last raiseed response as an attribute" do
|
293
|
+
err = nil
|
294
|
+
begin
|
295
|
+
perform(should_retry: ->(*) { true })
|
296
|
+
rescue described_class => e
|
297
|
+
err = e
|
298
|
+
end
|
299
|
+
expect(err.response).to be response
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::URI::NORMALIZER do
|
4
|
+
describe "scheme" do
|
5
|
+
it "lower-cases scheme" do
|
6
|
+
expect(HTTP::URI::NORMALIZER.call("HttP://example.com").scheme).to eq "http"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "hostname" do
|
11
|
+
it "lower-cases hostname" do
|
12
|
+
expect(HTTP::URI::NORMALIZER.call("http://EXAMPLE.com").host).to eq "example.com"
|
13
|
+
end
|
14
|
+
|
15
|
+
it "decodes percent-encoded hostname" do
|
16
|
+
expect(HTTP::URI::NORMALIZER.call("http://ex%61mple.com").host).to eq "example.com"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "removes trailing period in hostname" do
|
20
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com.").host).to eq "example.com"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "IDN-encodes non-ASCII hostname" do
|
24
|
+
expect(HTTP::URI::NORMALIZER.call("http://exämple.com").host).to eq "xn--exmple-cua.com"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "path" do
|
29
|
+
it "ensures path is not empty" do
|
30
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com").path).to eq "/"
|
31
|
+
end
|
32
|
+
|
33
|
+
it "preserves double slashes in path" do
|
34
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com//a///b").path).to eq "//a///b"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "resolves single-dot segments in path" do
|
38
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/a/./b").path).to eq "/a/b"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "resolves double-dot segments in path" do
|
42
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/a/b/../c").path).to eq "/a/c"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "resolves leading double-dot segments in path" do
|
46
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/../a/b").path).to eq "/a/b"
|
47
|
+
end
|
48
|
+
|
49
|
+
it "percent-encodes control characters in path" do
|
50
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/\x00\x7F\n").path).to eq "/%00%7F%0A"
|
51
|
+
end
|
52
|
+
|
53
|
+
it "percent-encodes space in path" do
|
54
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/a b").path).to eq "/a%20b"
|
55
|
+
end
|
56
|
+
|
57
|
+
it "percent-encodes non-ASCII characters in path" do
|
58
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/キョ").path).to eq "/%E3%82%AD%E3%83%A7"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "does not percent-encode non-special characters in path" do
|
62
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/~.-_!$&()*,;=:@{}").path).to eq "/~.-_!$&()*,;=:@{}"
|
63
|
+
end
|
64
|
+
|
65
|
+
it "preserves escape sequences in path" do
|
66
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/%41").path).to eq "/%41"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "query" do
|
71
|
+
it "allows no query" do
|
72
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com").query).to be_nil
|
73
|
+
end
|
74
|
+
|
75
|
+
it "percent-encodes control characters in query" do
|
76
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/?\x00\x7F\n").query).to eq "%00%7F%0A"
|
77
|
+
end
|
78
|
+
|
79
|
+
it "percent-encodes space in query" do
|
80
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/?a b").query).to eq "a%20b"
|
81
|
+
end
|
82
|
+
|
83
|
+
it "percent-encodes non-ASCII characters in query" do
|
84
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com?キョ").query).to eq "%E3%82%AD%E3%83%A7"
|
85
|
+
end
|
86
|
+
|
87
|
+
it "does not percent-encode non-special characters in query" do
|
88
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/?~.-_!$&()*,;=:@{}?").query).to eq "~.-_!$&()*,;=:@{}?"
|
89
|
+
end
|
90
|
+
|
91
|
+
it "preserves escape sequences in query" do
|
92
|
+
expect(HTTP::URI::NORMALIZER.call("http://example.com/?%41").query).to eq "%41"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/spec/lib/http_spec.rb
CHANGED
@@ -152,6 +152,35 @@ RSpec.describe HTTP do
|
|
152
152
|
end
|
153
153
|
end
|
154
154
|
|
155
|
+
describe ".retry" do
|
156
|
+
it "ensure endpoint counts retries" do
|
157
|
+
expect(HTTP.get("#{dummy.endpoint}/retry-2").to_s).to eq "retried 1x"
|
158
|
+
expect(HTTP.get("#{dummy.endpoint}/retry-2").to_s).to eq "retried 2x"
|
159
|
+
end
|
160
|
+
|
161
|
+
it "retries the request" do
|
162
|
+
response = HTTP.retriable(delay: 0, retry_statuses: 500...600).get "#{dummy.endpoint}/retry-2"
|
163
|
+
expect(response.to_s).to eq "retried 2x"
|
164
|
+
end
|
165
|
+
|
166
|
+
it "retries the request and gives us access to the failed requests" do
|
167
|
+
err = nil
|
168
|
+
retry_callback = ->(_, _, res) { expect(res.to_s).to match(/^retried \dx$/) }
|
169
|
+
begin
|
170
|
+
HTTP.retriable(
|
171
|
+
should_retry: ->(*) { true },
|
172
|
+
tries: 3,
|
173
|
+
delay: 0,
|
174
|
+
on_retry: retry_callback
|
175
|
+
).get "#{dummy.endpoint}/retry-2"
|
176
|
+
rescue HTTP::Error => e
|
177
|
+
err = e
|
178
|
+
end
|
179
|
+
|
180
|
+
expect(err.response.to_s).to eq "retried 3x"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
155
184
|
context "posting forms to resources" do
|
156
185
|
it "is easy" do
|
157
186
|
response = HTTP.post "#{dummy.endpoint}/form", :form => {:example => "testing-form"}
|
@@ -460,20 +489,37 @@ RSpec.describe HTTP do
|
|
460
489
|
|
461
490
|
context "with :normalize_uri" do
|
462
491
|
it "normalizes URI" do
|
463
|
-
response = HTTP.get "#{dummy.endpoint}/
|
492
|
+
response = HTTP.get "#{dummy.endpoint}/héllö-wörld"
|
464
493
|
expect(response.to_s).to eq("hello world")
|
465
494
|
end
|
466
495
|
|
467
496
|
it "uses the custom URI Normalizer method" do
|
468
497
|
client = HTTP.use(:normalize_uri => {:normalizer => :itself.to_proc})
|
469
|
-
response = client.get("#{dummy.endpoint}/
|
498
|
+
response = client.get("#{dummy.endpoint}/héllö-wörld")
|
470
499
|
expect(response.status).to eq(400)
|
471
500
|
end
|
472
501
|
|
502
|
+
it "raises if custom URI Normalizer returns invalid path" do
|
503
|
+
client = HTTP.use(:normalize_uri => {:normalizer => :itself.to_proc})
|
504
|
+
expect { client.get("#{dummy.endpoint}/hello\nworld") }.
|
505
|
+
to raise_error HTTP::RequestError, 'Invalid request URI: "/hello\nworld"'
|
506
|
+
end
|
507
|
+
|
508
|
+
it "raises if custom URI Normalizer returns invalid host" do
|
509
|
+
normalizer = lambda do |uri|
|
510
|
+
uri.port = nil
|
511
|
+
uri.instance_variable_set(:@host, "example\ncom")
|
512
|
+
uri
|
513
|
+
end
|
514
|
+
client = HTTP.use(:normalize_uri => {:normalizer => normalizer})
|
515
|
+
expect { client.get(dummy.endpoint) }.
|
516
|
+
to raise_error HTTP::RequestError, 'Invalid host: "example\ncom"'
|
517
|
+
end
|
518
|
+
|
473
519
|
it "uses the default URI normalizer" do
|
474
520
|
client = HTTP.use :normalize_uri
|
475
521
|
expect(HTTP::URI::NORMALIZER).to receive(:call).and_call_original
|
476
|
-
response = client.get("#{dummy.endpoint}/
|
522
|
+
response = client.get("#{dummy.endpoint}/héllö-wörld")
|
477
523
|
expect(response.to_s).to eq("hello world")
|
478
524
|
end
|
479
525
|
end
|