http.rb 0.18.3 → 0.20.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 +21 -0
- data/README.md +32 -0
- data/lib/HTTP/RETRY.rb +76 -0
- data/lib/HTTP/VERSION.rb +1 -1
- data/lib/HTTP/request.rb +11 -3
- data/spec/HTTP/RETRY_spec.rb +251 -0
- data/spec/HTTP/delete_spec.rb +5 -5
- data/spec/HTTP/get_spec.rb +31 -5
- data/spec/HTTP/post_spec.rb +6 -6
- data/spec/HTTP/put_spec.rb +5 -5
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8926fd82491288941f05e4ff3d38c0c000e31f214bd35f55462befa0ece73a3
|
|
4
|
+
data.tar.gz: ad696135489b778872649c052fb1f23eb05e151e7a04c29237510e997dba7d71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eb7f2d8945396de4151166fb3a2fc62a0bb47ac2636a9f157b04c67318f0c9c20d302aba00b8a0db21278fbeed559230b236fea674cb2cf4de3880fe22b09dce
|
|
7
|
+
data.tar.gz: bea5d01ffdab8e4a8fd876ec1b7353776b933d5e1259ea8c57aa4cbf4a8a6b3ba35950fa7bdc907b18bff84e5e7cbc972abbd1cc71398f37934834f176e9d86c
|
data/CHANGELOG
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
# 20260522
|
|
4
|
+
# 0.20.0: Add opt-in retry logic with exponential backoff.
|
|
5
|
+
1. + lib/HTTP/RETRY.rb: Retry helpers (with_retries, backoff_delay, retry_after) and constants (HTTP::RETRY::EXCEPTIONS, STATUS_CODES, VERBS).
|
|
6
|
+
2. ~ HTTP.request: Retry on transient network exceptions and retry-worthy HTTP status codes (429, 502, 503, 504) when enabled. Exponential backoff with jitter. Honours Retry-After when present. Disabled by default; opt in via the retries option. Configurable via new options: retries (default 0), retry_delay (default 1.0), retry_status_codes, retry_exceptions, retry_verbs. Only idempotent verbs (get, head, options, put, delete, trace) retry by default; opt in to POST/PATCH retries via retry_verbs.
|
|
7
|
+
3. + spec/HTTP/RETRY_spec.rb: Specs for retry behaviour and the helpers.
|
|
8
|
+
4. ~ README.md: + Retries section.
|
|
9
|
+
5. ~ TODO: Annotate "Add retry logic to HTTP.request" as (Done as of 0.20.0); also annotate three previously-completed items as (Done as of 0.18.2), (Done as of 0.18.3), and (Done as of 0.19.0).
|
|
10
|
+
6. ~ HTTP::VERSION: /0.19.0/0.20.0/
|
|
11
|
+
7. ~ CHANGELOG: + 0.20.0 entry
|
|
12
|
+
|
|
13
|
+
# 20260522
|
|
14
|
+
# 0.19.0: Change default verify_mode to VERIFY_PEER.
|
|
15
|
+
1. ~ HTTP.request: Default verify_mode changed from OpenSSL::SSL::VERIFY_NONE to OpenSSL::SSL::VERIFY_PEER. Callers needing the old behaviour can pass verify_mode: OpenSSL::SSL::VERIFY_NONE explicitly through the options hash.
|
|
16
|
+
2. ~ spec/HTTP/get_spec.rb: + specs for default verify_mode and explicit VERIFY_NONE override; /verify_mode: 0/verify_mode: OpenSSL::SSL::VERIFY_PEER/ in redirect specs.
|
|
17
|
+
3. ~ spec/HTTP/post_spec.rb: /verify_mode: 0/verify_mode: OpenSSL::SSL::VERIFY_PEER/ in redirect specs.
|
|
18
|
+
4. ~ spec/HTTP/put_spec.rb: /verify_mode: 0/verify_mode: OpenSSL::SSL::VERIFY_PEER/ in redirect specs.
|
|
19
|
+
5. ~ spec/HTTP/delete_spec.rb: /verify_mode: 0/verify_mode: OpenSSL::SSL::VERIFY_PEER/ in redirect specs.
|
|
20
|
+
6. ~ README.md: Note the new verify_mode default and how to opt back into VERIFY_NONE.
|
|
21
|
+
7. ~ HTTP::VERSION: /0.18.3/0.19.0/
|
|
22
|
+
8. ~ CHANGELOG: + 0.19.0 entry
|
|
23
|
+
|
|
3
24
|
# 20260521
|
|
4
25
|
# 0.18.3: Fix verb preservation on 307/308 redirects.
|
|
5
26
|
1. ~ HTTP.request: Use original verb when following 307 or 308 redirects, per RFC 7231 §6.4.7 and RFC 7538. 301/302/303 keep legacy GET-on-redirect behaviour.
|
data/README.md
CHANGED
|
@@ -84,6 +84,34 @@ HTTP.get('http://example.com', {}, {}, {no_redirect: true})
|
|
|
84
84
|
# => #<Net::HTTPResponse @code=3xx>
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
+
### Retries
|
|
88
|
+
|
|
89
|
+
Retries are disabled by default. Enable them by passing `retries:` in the options hash.
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
HTTP.get('http://example.com', {}, {}, {retries: 3})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
When enabled, transient network exceptions and retry-worthy HTTP status codes (429, 502, 503, 504) are retried with exponential backoff and jitter. If the response carries a `Retry-After` header, it is honoured in place of the calculated delay.
|
|
96
|
+
|
|
97
|
+
Only idempotent verbs (`get`, `head`, `options`, `put`, `delete`, `trace`) are retried by default. POST and PATCH are not — retrying a non-idempotent write can create duplicate resources against APIs that don't deduplicate. Opt in per-call via `retry_verbs:`.
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
HTTP.post('http://example.com', {a: 1}, {}, {retries: 3, retry_verbs: %i{get post}})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Configurable options:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
options = {
|
|
107
|
+
retries: 3, # max retry attempts; 0 disables
|
|
108
|
+
retry_delay: 1.0, # base delay (seconds) for exponential backoff
|
|
109
|
+
retry_status_codes: [429, 502, 503, 504], # HTTP status codes to retry
|
|
110
|
+
retry_exceptions: HTTP::RETRY::EXCEPTIONS, # exceptions to retry
|
|
111
|
+
retry_verbs: HTTP::RETRY::VERBS # verbs that retry by default
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
87
115
|
### Response status predicate methods
|
|
88
116
|
|
|
89
117
|
```ruby
|
|
@@ -241,6 +269,10 @@ verify_mode
|
|
|
241
269
|
# SSL/TLS session.
|
|
242
270
|
#
|
|
243
271
|
# OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable.
|
|
272
|
+
#
|
|
273
|
+
# Defaults to OpenSSL::SSL::VERIFY_PEER as of 0.19.0. To opt back into the
|
|
274
|
+
# previous behaviour, pass verify_mode: OpenSSL::SSL::VERIFY_NONE through
|
|
275
|
+
# the options hash.
|
|
244
276
|
```
|
|
245
277
|
|
|
246
278
|
## Contributing
|
data/lib/HTTP/RETRY.rb
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# HTTP/RETRY.rb
|
|
2
|
+
# HTTP::RETRY (retry helpers)
|
|
3
|
+
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'socket'
|
|
6
|
+
require 'time'
|
|
7
|
+
|
|
8
|
+
module HTTP
|
|
9
|
+
module RETRY
|
|
10
|
+
EXCEPTIONS = [
|
|
11
|
+
Errno::ECONNREFUSED,
|
|
12
|
+
Errno::ECONNRESET,
|
|
13
|
+
Errno::ETIMEDOUT,
|
|
14
|
+
Errno::EHOSTUNREACH,
|
|
15
|
+
Errno::ENETUNREACH,
|
|
16
|
+
Net::OpenTimeout,
|
|
17
|
+
Net::ReadTimeout,
|
|
18
|
+
SocketError,
|
|
19
|
+
EOFError
|
|
20
|
+
].freeze
|
|
21
|
+
STATUS_CODES = [429, 502, 503, 504].freeze
|
|
22
|
+
VERBS = %i{get head options put delete trace}.freeze
|
|
23
|
+
|
|
24
|
+
def self.sleep(seconds)
|
|
25
|
+
Kernel.sleep(seconds)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def retry_config(options)
|
|
30
|
+
{
|
|
31
|
+
retries: options.delete(:retries) || 0,
|
|
32
|
+
delay: options.delete(:retry_delay) || 1.0,
|
|
33
|
+
status_codes: options.delete(:retry_status_codes) || RETRY::STATUS_CODES,
|
|
34
|
+
exceptions: options.delete(:retry_exceptions) || RETRY::EXCEPTIONS,
|
|
35
|
+
verbs: options.delete(:retry_verbs) || RETRY::VERBS
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
module_function :retry_config
|
|
39
|
+
|
|
40
|
+
def with_retries(http, request_object, config)
|
|
41
|
+
attempt = 0
|
|
42
|
+
loop do
|
|
43
|
+
begin
|
|
44
|
+
response = http.request(request_object)
|
|
45
|
+
if config[:status_codes].include?(response.code.to_i) && attempt < config[:retries]
|
|
46
|
+
attempt += 1
|
|
47
|
+
RETRY.sleep(retry_after(response) || backoff_delay(config[:delay], attempt))
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
return response
|
|
51
|
+
rescue *config[:exceptions]
|
|
52
|
+
raise unless attempt < config[:retries]
|
|
53
|
+
attempt += 1
|
|
54
|
+
RETRY.sleep(backoff_delay(config[:delay], attempt))
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
module_function :with_retries
|
|
59
|
+
|
|
60
|
+
def backoff_delay(base, attempt)
|
|
61
|
+
base * (2 ** (attempt - 1)) * (1 + (rand - 0.5) * 0.4)
|
|
62
|
+
end
|
|
63
|
+
module_function :backoff_delay
|
|
64
|
+
|
|
65
|
+
def retry_after(response)
|
|
66
|
+
header = response['Retry-After']
|
|
67
|
+
return nil unless header
|
|
68
|
+
if header =~ /\A\d+\z/
|
|
69
|
+
header.to_i
|
|
70
|
+
else
|
|
71
|
+
# Malformed HTTP-date — fall through to caller's backoff.
|
|
72
|
+
Time.httpdate(header) - Time.now rescue nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
module_function :retry_after
|
|
76
|
+
end
|
data/lib/HTTP/VERSION.rb
CHANGED
data/lib/HTTP/request.rb
CHANGED
|
@@ -9,18 +9,27 @@ require_relative '../Net/HTTP/set_options'
|
|
|
9
9
|
require_relative '../Net/HTTPRequest/set_headers'
|
|
10
10
|
require_relative '../Net/HTTPResponse/StatusPredicates'
|
|
11
11
|
require_relative '../URI/Generic/use_sslQ'
|
|
12
|
+
require_relative './RETRY'
|
|
12
13
|
|
|
13
14
|
module HTTP
|
|
14
15
|
def request(uri, request_object, headers = {}, options = {}, &block)
|
|
15
16
|
uri = uri.is_a?(URI) ? uri : URI.parse(uri)
|
|
16
17
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
17
18
|
no_redirect = options.delete(:no_redirect)
|
|
19
|
+
config = retry_config(options)
|
|
18
20
|
options[:use_ssl] ||= uri.use_ssl?
|
|
19
|
-
options[:verify_mode] ||= OpenSSL::SSL::
|
|
21
|
+
options[:verify_mode] ||= OpenSSL::SSL::VERIFY_PEER
|
|
20
22
|
http.options = options
|
|
21
23
|
request_object.headers = headers
|
|
22
24
|
request_object.basic_auth(uri.user, uri.password) if uri.user
|
|
23
|
-
|
|
25
|
+
verb = request_object.method.downcase.to_sym
|
|
26
|
+
response = (
|
|
27
|
+
if config[:retries] > 0 && config[:verbs].include?(verb)
|
|
28
|
+
with_retries(http, request_object, config)
|
|
29
|
+
else
|
|
30
|
+
http.request(request_object)
|
|
31
|
+
end
|
|
32
|
+
)
|
|
24
33
|
if response.code =~ /^3/
|
|
25
34
|
if block_given? && no_redirect
|
|
26
35
|
yield response
|
|
@@ -29,7 +38,6 @@ module HTTP
|
|
|
29
38
|
end
|
|
30
39
|
redirect_uri = uri.merge(response.header['location'])
|
|
31
40
|
if response.code =~ /^30[78]$/
|
|
32
|
-
verb = request_object.method.downcase.to_sym
|
|
33
41
|
data = VERBS::WITH_BODY.include?(verb) ? request_object.body : {}
|
|
34
42
|
response = send(verb, redirect_uri.to_s, data, headers, options, &block)
|
|
35
43
|
else
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# spec/HTTP/RETRY_spec.rb
|
|
2
|
+
|
|
3
|
+
require_relative '../spec_helper'
|
|
4
|
+
require 'http'
|
|
5
|
+
|
|
6
|
+
describe "retry behaviour" do
|
|
7
|
+
let(:uri){'http://example.com/path'}
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
allow(HTTP::RETRY).to receive(:sleep)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe "defaults" do
|
|
14
|
+
it "does not retry by default" do
|
|
15
|
+
stub_request(:get, uri).to_return(status: 503)
|
|
16
|
+
response = HTTP.get(uri)
|
|
17
|
+
expect(response.code.to_i).to eq(503)
|
|
18
|
+
expect(WebMock).to have_requested(:get, uri).times(1)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "does not retry on a transient exception by default" do
|
|
22
|
+
stub_request(:get, uri).to_raise(Errno::ECONNRESET)
|
|
23
|
+
expect{HTTP.get(uri)}.to raise_error(Errno::ECONNRESET)
|
|
24
|
+
expect(WebMock).to have_requested(:get, uri).times(1)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "retry on transient exception" do
|
|
29
|
+
it "retries and succeeds when the failure is transient" do
|
|
30
|
+
stub_request(:get, uri).
|
|
31
|
+
to_raise(Errno::ECONNRESET).then.
|
|
32
|
+
to_raise(Errno::ECONNRESET).then.
|
|
33
|
+
to_return(status: 200, body: '')
|
|
34
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
35
|
+
expect(response.success?).to eq(true)
|
|
36
|
+
expect(WebMock).to have_requested(:get, uri).times(3)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "re-raises the exception after retries are exhausted" do
|
|
40
|
+
stub_request(:get, uri).to_raise(Errno::ECONNRESET)
|
|
41
|
+
expect{HTTP.get(uri, {}, {}, {retries: 2})}.to raise_error(Errno::ECONNRESET)
|
|
42
|
+
expect(WebMock).to have_requested(:get, uri).times(3)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "retries on SocketError (DNS failure)" do
|
|
46
|
+
stub_request(:get, uri).
|
|
47
|
+
to_raise(SocketError).then.
|
|
48
|
+
to_return(status: 200, body: '')
|
|
49
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
50
|
+
expect(response.success?).to eq(true)
|
|
51
|
+
expect(WebMock).to have_requested(:get, uri).times(2)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "does not retry on a non-listed exception" do
|
|
55
|
+
stub_request(:get, uri).to_raise(OpenSSL::SSL::SSLError)
|
|
56
|
+
expect{HTTP.get(uri, {}, {}, {retries: 3})}.to raise_error(OpenSSL::SSL::SSLError)
|
|
57
|
+
expect(WebMock).to have_requested(:get, uri).times(1)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe "retry on status code" do
|
|
62
|
+
it "retries on 503 then succeeds" do
|
|
63
|
+
stub_request(:get, uri).
|
|
64
|
+
to_return({status: 503}, {status: 503}, {status: 200, body: ''})
|
|
65
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
66
|
+
expect(response.success?).to eq(true)
|
|
67
|
+
expect(WebMock).to have_requested(:get, uri).times(3)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "retries on 502" do
|
|
71
|
+
stub_request(:get, uri).to_return({status: 502}, {status: 200, body: ''})
|
|
72
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
73
|
+
expect(response.success?).to eq(true)
|
|
74
|
+
expect(WebMock).to have_requested(:get, uri).times(2)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "retries on 504" do
|
|
78
|
+
stub_request(:get, uri).to_return({status: 504}, {status: 200, body: ''})
|
|
79
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
80
|
+
expect(response.success?).to eq(true)
|
|
81
|
+
expect(WebMock).to have_requested(:get, uri).times(2)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "does not retry on 500 by default" do
|
|
85
|
+
stub_request(:get, uri).to_return(status: 500)
|
|
86
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
87
|
+
expect(response.code.to_i).to eq(500)
|
|
88
|
+
expect(WebMock).to have_requested(:get, uri).times(1)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "does not retry on 404" do
|
|
92
|
+
stub_request(:get, uri).to_return(status: 404)
|
|
93
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
94
|
+
expect(response.code.to_i).to eq(404)
|
|
95
|
+
expect(WebMock).to have_requested(:get, uri).times(1)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "returns the last response when retries are exhausted" do
|
|
99
|
+
stub_request(:get, uri).to_return(status: 503)
|
|
100
|
+
response = HTTP.get(uri, {}, {}, {retries: 2})
|
|
101
|
+
expect(response.code.to_i).to eq(503)
|
|
102
|
+
expect(WebMock).to have_requested(:get, uri).times(3)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
describe "Retry-After header" do
|
|
107
|
+
it "honours integer Retry-After on 429" do
|
|
108
|
+
stub_request(:get, uri).
|
|
109
|
+
to_return({status: 429, headers: {'Retry-After' => '2'}}, {status: 200, body: ''})
|
|
110
|
+
expect(HTTP::RETRY).to receive(:sleep).with(2)
|
|
111
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
112
|
+
expect(response.success?).to eq(true)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it "honours integer Retry-After on 503" do
|
|
116
|
+
stub_request(:get, uri).
|
|
117
|
+
to_return({status: 503, headers: {'Retry-After' => '5'}}, {status: 200, body: ''})
|
|
118
|
+
expect(HTTP::RETRY).to receive(:sleep).with(5)
|
|
119
|
+
response = HTTP.get(uri, {}, {}, {retries: 3})
|
|
120
|
+
expect(response.success?).to eq(true)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
describe "configuration" do
|
|
125
|
+
it "treats retries: 0 as no retries" do
|
|
126
|
+
stub_request(:get, uri).to_return(status: 503)
|
|
127
|
+
response = HTTP.get(uri, {}, {}, {retries: 0})
|
|
128
|
+
expect(response.code.to_i).to eq(503)
|
|
129
|
+
expect(WebMock).to have_requested(:get, uri).times(1)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "respects a custom retry_status_codes list" do
|
|
133
|
+
stub_request(:get, uri).to_return({status: 500}, {status: 200, body: ''})
|
|
134
|
+
response = HTTP.get(uri, {}, {}, {retries: 3, retry_status_codes: [500]})
|
|
135
|
+
expect(response.success?).to eq(true)
|
|
136
|
+
expect(WebMock).to have_requested(:get, uri).times(2)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "respects a custom retry_exceptions list" do
|
|
140
|
+
stub_request(:get, uri).
|
|
141
|
+
to_raise(OpenSSL::SSL::SSLError).then.
|
|
142
|
+
to_return(status: 200, body: '')
|
|
143
|
+
response = HTTP.get(uri, {}, {}, {retries: 3, retry_exceptions: [OpenSSL::SSL::SSLError]})
|
|
144
|
+
expect(response.success?).to eq(true)
|
|
145
|
+
expect(WebMock).to have_requested(:get, uri).times(2)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "does not pass retry options through to Net::HTTP" do
|
|
149
|
+
stub_request(:get, uri).to_return(status: 200, body: '')
|
|
150
|
+
net_http_object = Net::HTTP.new(URI.parse(uri).host, URI.parse(uri).port)
|
|
151
|
+
allow(Net::HTTP).to receive(:new).and_return(net_http_object)
|
|
152
|
+
expect(net_http_object).to receive(:options=) do |opts|
|
|
153
|
+
expect(opts).not_to include(:retries, :retry_delay, :retry_status_codes, :retry_exceptions, :retry_verbs)
|
|
154
|
+
end
|
|
155
|
+
HTTP.get(uri, {}, {}, {
|
|
156
|
+
retries: 3,
|
|
157
|
+
retry_delay: 0.1,
|
|
158
|
+
retry_status_codes: [500],
|
|
159
|
+
retry_exceptions: [Errno::ECONNRESET],
|
|
160
|
+
retry_verbs: %i{get}
|
|
161
|
+
})
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
describe "backoff timing" do
|
|
166
|
+
it "increases the delay between successive retries" do
|
|
167
|
+
delays = []
|
|
168
|
+
allow(HTTP::RETRY).to receive(:sleep){|d| delays << d}
|
|
169
|
+
stub_request(:get, uri).to_return(status: 503)
|
|
170
|
+
HTTP.get(uri, {}, {}, {retries: 3, retry_delay: 1.0})
|
|
171
|
+
expect(delays.length).to eq(3)
|
|
172
|
+
expect(delays[1]).to be > delays[0] * 0.8
|
|
173
|
+
expect(delays[2]).to be > delays[1] * 0.8
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
describe "verb-based retry default" do
|
|
178
|
+
it "does not retry POST by default even when retries are enabled" do
|
|
179
|
+
stub_request(:post, uri).to_return(status: 503)
|
|
180
|
+
HTTP.post(uri, {}, {}, {retries: 3})
|
|
181
|
+
expect(WebMock).to have_requested(:post, uri).times(1)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "does not retry PATCH by default" do
|
|
185
|
+
stub_request(:patch, uri).to_return(status: 503)
|
|
186
|
+
HTTP.patch(uri, {}, {}, {retries: 3})
|
|
187
|
+
expect(WebMock).to have_requested(:patch, uri).times(1)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it "retries PUT by default (idempotent)" do
|
|
191
|
+
stub_request(:put, uri).to_return({status: 503}, {status: 200, body: ''})
|
|
192
|
+
response = HTTP.put(uri, {}, {}, {retries: 3})
|
|
193
|
+
expect(response.success?).to eq(true)
|
|
194
|
+
expect(WebMock).to have_requested(:put, uri).times(2)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "retries DELETE by default (idempotent)" do
|
|
198
|
+
stub_request(:delete, uri).to_return({status: 503}, {status: 200, body: ''})
|
|
199
|
+
response = HTTP.delete(uri, {}, {}, {retries: 3})
|
|
200
|
+
expect(response.success?).to eq(true)
|
|
201
|
+
expect(WebMock).to have_requested(:delete, uri).times(2)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
it "retries POST when opted in via retry_verbs" do
|
|
205
|
+
stub_request(:post, uri).to_return({status: 503}, {status: 200, body: ''})
|
|
206
|
+
response = HTTP.post(uri, {}, {}, {retries: 3, retry_verbs: %i{get post}})
|
|
207
|
+
expect(response.success?).to eq(true)
|
|
208
|
+
expect(WebMock).to have_requested(:post, uri).times(2)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
describe HTTP, ".retry_after" do
|
|
214
|
+
it "returns integer seconds for a delta-seconds Retry-After header" do
|
|
215
|
+
response = instance_double(Net::HTTPResponse)
|
|
216
|
+
allow(response).to receive(:[]).with('Retry-After').and_return('5')
|
|
217
|
+
expect(HTTP.retry_after(response)).to eq(5)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it "parses an HTTP-date Retry-After header" do
|
|
221
|
+
base = Time.utc(2026, 5, 22, 12, 0, 0)
|
|
222
|
+
retry_at_header = (base + 5).httpdate
|
|
223
|
+
response = instance_double(Net::HTTPResponse)
|
|
224
|
+
allow(response).to receive(:[]).with('Retry-After').and_return(retry_at_header)
|
|
225
|
+
allow(Time).to receive(:now).and_return(base)
|
|
226
|
+
expect(HTTP.retry_after(response)).to be_within(0.001).of(5.0)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it "returns nil when Retry-After is absent" do
|
|
230
|
+
response = instance_double(Net::HTTPResponse)
|
|
231
|
+
allow(response).to receive(:[]).with('Retry-After').and_return(nil)
|
|
232
|
+
expect(HTTP.retry_after(response)).to be_nil
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it "returns nil when Retry-After is malformed" do
|
|
236
|
+
response = instance_double(Net::HTTPResponse)
|
|
237
|
+
allow(response).to receive(:[]).with('Retry-After').and_return('not a date')
|
|
238
|
+
expect(HTTP.retry_after(response)).to be_nil
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
describe HTTP, ".backoff_delay" do
|
|
243
|
+
it "grows exponentially with attempt number" do
|
|
244
|
+
base = 1.0
|
|
245
|
+
delays = (1..4).map{|attempt| HTTP.backoff_delay(base, attempt)}
|
|
246
|
+
expect(delays[0]).to be_within(0.2).of(1.0)
|
|
247
|
+
expect(delays[1]).to be_within(0.4).of(2.0)
|
|
248
|
+
expect(delays[2]).to be_within(0.8).of(4.0)
|
|
249
|
+
expect(delays[3]).to be_within(1.6).of(8.0)
|
|
250
|
+
end
|
|
251
|
+
end
|
data/spec/HTTP/delete_spec.rb
CHANGED
|
@@ -148,7 +148,7 @@ describe ".delete" do
|
|
|
148
148
|
|
|
149
149
|
it "does a redirect" do
|
|
150
150
|
expect(HTTP).to receive(:delete).once.with(request_uri).and_call_original
|
|
151
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
151
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
152
152
|
response = HTTP.delete(request_uri)
|
|
153
153
|
expect(response.success?).to eq(true)
|
|
154
154
|
end
|
|
@@ -163,7 +163,7 @@ describe ".delete" do
|
|
|
163
163
|
|
|
164
164
|
it "does a redirect" do
|
|
165
165
|
expect(HTTP).to receive(:delete).once.with(request_uri).and_call_original
|
|
166
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
166
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
167
167
|
response = HTTP.delete(request_uri)
|
|
168
168
|
expect(response.success?).to eq(true)
|
|
169
169
|
end
|
|
@@ -190,7 +190,7 @@ describe ".delete" do
|
|
|
190
190
|
|
|
191
191
|
it "does a redirect" do
|
|
192
192
|
expect(HTTP).to receive(:delete).once.with(request_uri).and_call_original
|
|
193
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
193
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
194
194
|
response = HTTP.delete(request_uri)
|
|
195
195
|
expect(response.success?).to eq(true)
|
|
196
196
|
end
|
|
@@ -205,7 +205,7 @@ describe ".delete" do
|
|
|
205
205
|
|
|
206
206
|
it "does a redirect" do
|
|
207
207
|
expect(HTTP).to receive(:delete).once.with(request_uri).and_call_original
|
|
208
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
208
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
209
209
|
response = HTTP.delete(request_uri)
|
|
210
210
|
expect(response.success?).to eq(true)
|
|
211
211
|
end
|
|
@@ -227,7 +227,7 @@ describe ".delete" do
|
|
|
227
227
|
|
|
228
228
|
it "preserves the verb" do
|
|
229
229
|
expect(HTTP).to receive(:delete).with(request_uri).and_call_original.ordered
|
|
230
|
-
expect(HTTP).to receive(:delete).with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
230
|
+
expect(HTTP).to receive(:delete).with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original.ordered
|
|
231
231
|
response = HTTP.delete(request_uri)
|
|
232
232
|
expect(response.success?).to eq(true)
|
|
233
233
|
end
|
data/spec/HTTP/get_spec.rb
CHANGED
|
@@ -115,6 +115,32 @@ describe ".get" do
|
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
+
context "with default verify_mode" do
|
|
119
|
+
let(:uri){'http://example.com/path'}
|
|
120
|
+
let(:parsed_uri){URI.parse(uri)}
|
|
121
|
+
let(:net_http_object){Net::HTTP.new(parsed_uri.host, parsed_uri.port)}
|
|
122
|
+
|
|
123
|
+
before do
|
|
124
|
+
stub_request(:get, uri).
|
|
125
|
+
with(headers: {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'User-Agent'=>'Ruby'}).
|
|
126
|
+
to_return(status: 200, body: '', headers: {})
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "defaults verify_mode to OpenSSL::SSL::VERIFY_PEER" do
|
|
130
|
+
allow(Net::HTTP).to receive(:new).with(parsed_uri.host, parsed_uri.port).and_return(net_http_object)
|
|
131
|
+
response = HTTP.get(uri)
|
|
132
|
+
expect(net_http_object.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER)
|
|
133
|
+
expect(response.success?).to eq(true)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it "allows opting back into VERIFY_NONE via options" do
|
|
137
|
+
allow(Net::HTTP).to receive(:new).with(parsed_uri.host, parsed_uri.port).and_return(net_http_object)
|
|
138
|
+
response = HTTP.get(uri, {}, {}, {verify_mode: OpenSSL::SSL::VERIFY_NONE})
|
|
139
|
+
expect(net_http_object.verify_mode).to eq(OpenSSL::SSL::VERIFY_NONE)
|
|
140
|
+
expect(response.success?).to eq(true)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
118
144
|
context "with block supplied" do
|
|
119
145
|
let(:uri){'http://example.com/path'}
|
|
120
146
|
|
|
@@ -148,7 +174,7 @@ describe ".get" do
|
|
|
148
174
|
|
|
149
175
|
it "does a redirect" do
|
|
150
176
|
expect(HTTP).to receive(:get).once.with(request_uri).and_call_original
|
|
151
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
177
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
152
178
|
response = HTTP.get(request_uri)
|
|
153
179
|
expect(response.success?).to eq(true)
|
|
154
180
|
end
|
|
@@ -163,7 +189,7 @@ describe ".get" do
|
|
|
163
189
|
|
|
164
190
|
it "does a redirect" do
|
|
165
191
|
expect(HTTP).to receive(:get).once.with(request_uri).and_call_original
|
|
166
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
192
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
167
193
|
response = HTTP.get(request_uri)
|
|
168
194
|
expect(response.success?).to eq(true)
|
|
169
195
|
end
|
|
@@ -190,7 +216,7 @@ describe ".get" do
|
|
|
190
216
|
|
|
191
217
|
it "does a redirect" do
|
|
192
218
|
expect(HTTP).to receive(:get).once.with(request_uri).and_call_original
|
|
193
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
219
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
194
220
|
response = HTTP.get(request_uri)
|
|
195
221
|
expect(response.success?).to eq(true)
|
|
196
222
|
end
|
|
@@ -205,7 +231,7 @@ describe ".get" do
|
|
|
205
231
|
|
|
206
232
|
it "does a redirect" do
|
|
207
233
|
expect(HTTP).to receive(:get).once.with(request_uri).and_call_original
|
|
208
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
234
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
209
235
|
response = HTTP.get(request_uri)
|
|
210
236
|
expect(response.success?).to eq(true)
|
|
211
237
|
end
|
|
@@ -228,7 +254,7 @@ describe ".get" do
|
|
|
228
254
|
|
|
229
255
|
it "preserves the HTTPS scheme on a relative redirect" do
|
|
230
256
|
expect(HTTP).to receive(:get).once.with(request_uri).and_call_original
|
|
231
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: true, verify_mode:
|
|
257
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
232
258
|
response = HTTP.get(request_uri)
|
|
233
259
|
expect(response.success?).to eq(true)
|
|
234
260
|
end
|
data/spec/HTTP/post_spec.rb
CHANGED
|
@@ -286,7 +286,7 @@ describe ".post" do
|
|
|
286
286
|
|
|
287
287
|
it "does a redirect" do
|
|
288
288
|
expect(HTTP).to receive(:post).once.with(request_uri).and_call_original
|
|
289
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
289
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
290
290
|
response = HTTP.post(request_uri)
|
|
291
291
|
expect(response.success?).to eq(true)
|
|
292
292
|
end
|
|
@@ -301,7 +301,7 @@ describe ".post" do
|
|
|
301
301
|
|
|
302
302
|
it "does a redirect" do
|
|
303
303
|
expect(HTTP).to receive(:post).once.with(request_uri).and_call_original
|
|
304
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
304
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
305
305
|
response = HTTP.post(request_uri)
|
|
306
306
|
expect(response.success?).to eq(true)
|
|
307
307
|
end
|
|
@@ -328,7 +328,7 @@ describe ".post" do
|
|
|
328
328
|
|
|
329
329
|
it "does a redirect" do
|
|
330
330
|
expect(HTTP).to receive(:post).once.with(request_uri).and_call_original
|
|
331
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
331
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
332
332
|
response = HTTP.post(request_uri)
|
|
333
333
|
expect(response.success?).to eq(true)
|
|
334
334
|
end
|
|
@@ -343,7 +343,7 @@ describe ".post" do
|
|
|
343
343
|
|
|
344
344
|
it "does a redirect" do
|
|
345
345
|
expect(HTTP).to receive(:post).once.with(request_uri).and_call_original
|
|
346
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
346
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
347
347
|
response = HTTP.post(request_uri)
|
|
348
348
|
expect(response.success?).to eq(true)
|
|
349
349
|
end
|
|
@@ -369,7 +369,7 @@ describe ".post" do
|
|
|
369
369
|
|
|
370
370
|
it "preserves the verb" do
|
|
371
371
|
expect(HTTP).to receive(:post).with(request_uri).and_call_original.ordered
|
|
372
|
-
expect(HTTP).to receive(:post).with(redirect_uri, '', {}, {use_ssl: false, verify_mode:
|
|
372
|
+
expect(HTTP).to receive(:post).with(redirect_uri, '', {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original.ordered
|
|
373
373
|
response = HTTP.post(request_uri)
|
|
374
374
|
expect(response.success?).to eq(true)
|
|
375
375
|
end
|
|
@@ -384,7 +384,7 @@ describe ".post" do
|
|
|
384
384
|
|
|
385
385
|
it "preserves the verb" do
|
|
386
386
|
expect(HTTP).to receive(:post).with(request_uri).and_call_original.ordered
|
|
387
|
-
expect(HTTP).to receive(:post).with(redirect_uri, '', {}, {use_ssl: false, verify_mode:
|
|
387
|
+
expect(HTTP).to receive(:post).with(redirect_uri, '', {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original.ordered
|
|
388
388
|
response = HTTP.post(request_uri)
|
|
389
389
|
expect(response.success?).to eq(true)
|
|
390
390
|
end
|
data/spec/HTTP/put_spec.rb
CHANGED
|
@@ -286,7 +286,7 @@ describe ".put" do
|
|
|
286
286
|
|
|
287
287
|
it "does a redirect" do
|
|
288
288
|
expect(HTTP).to receive(:put).once.with(request_uri).and_call_original
|
|
289
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
289
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
290
290
|
response = HTTP.put(request_uri)
|
|
291
291
|
expect(response.success?).to eq(true)
|
|
292
292
|
end
|
|
@@ -301,7 +301,7 @@ describe ".put" do
|
|
|
301
301
|
|
|
302
302
|
it "does a redirect" do
|
|
303
303
|
expect(HTTP).to receive(:put).once.with(request_uri).and_call_original
|
|
304
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
304
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
305
305
|
response = HTTP.put(request_uri)
|
|
306
306
|
expect(response.success?).to eq(true)
|
|
307
307
|
end
|
|
@@ -328,7 +328,7 @@ describe ".put" do
|
|
|
328
328
|
|
|
329
329
|
it "does a redirect" do
|
|
330
330
|
expect(HTTP).to receive(:put).once.with(request_uri).and_call_original
|
|
331
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
331
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
332
332
|
response = HTTP.put(request_uri)
|
|
333
333
|
expect(response.success?).to eq(true)
|
|
334
334
|
end
|
|
@@ -343,7 +343,7 @@ describe ".put" do
|
|
|
343
343
|
|
|
344
344
|
it "does a redirect" do
|
|
345
345
|
expect(HTTP).to receive(:put).once.with(request_uri).and_call_original
|
|
346
|
-
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode:
|
|
346
|
+
expect(HTTP).to receive(:get).once.with(redirect_uri, {}, {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original
|
|
347
347
|
response = HTTP.put(request_uri)
|
|
348
348
|
expect(response.success?).to eq(true)
|
|
349
349
|
end
|
|
@@ -365,7 +365,7 @@ describe ".put" do
|
|
|
365
365
|
|
|
366
366
|
it "preserves the verb" do
|
|
367
367
|
expect(HTTP).to receive(:put).with(request_uri).and_call_original.ordered
|
|
368
|
-
expect(HTTP).to receive(:put).with(redirect_uri, '', {}, {use_ssl: false, verify_mode:
|
|
368
|
+
expect(HTTP).to receive(:put).with(redirect_uri, '', {}, {use_ssl: false, verify_mode: OpenSSL::SSL::VERIFY_PEER}).and_call_original.ordered
|
|
369
369
|
response = HTTP.put(request_uri)
|
|
370
370
|
expect(response.success?).to eq(true)
|
|
371
371
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: http.rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.20.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thoran
|
|
@@ -80,6 +80,7 @@ files:
|
|
|
80
80
|
- Rakefile
|
|
81
81
|
- http.rb.gemspec
|
|
82
82
|
- lib/HTTP.rb
|
|
83
|
+
- lib/HTTP/RETRY.rb
|
|
83
84
|
- lib/HTTP/VERSION.rb
|
|
84
85
|
- lib/HTTP/request.rb
|
|
85
86
|
- lib/HTTP/verbs.rb
|
|
@@ -93,6 +94,7 @@ files:
|
|
|
93
94
|
- lib/Thoran/Array/FirstX/firstX.rb
|
|
94
95
|
- lib/Thoran/String/ToConst/to_const.rb
|
|
95
96
|
- lib/URI/Generic/use_sslQ.rb
|
|
97
|
+
- spec/HTTP/RETRY_spec.rb
|
|
96
98
|
- spec/HTTP/delete_spec.rb
|
|
97
99
|
- spec/HTTP/get_spec.rb
|
|
98
100
|
- spec/HTTP/head_spec.rb
|