http 3.1.0 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +67 -0
- data/.gitignore +6 -9
- data/.rspec +0 -4
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/metrics.yml +4 -0
- data/.rubocop/rspec.yml +9 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop.yml +9 -108
- data/.rubocop_todo.yml +219 -0
- data/.yardopts +1 -1
- data/CHANGELOG.md +67 -0
- data/{CHANGES.md → CHANGES_OLD.md} +358 -0
- data/Gemfile +19 -10
- data/LICENSE.txt +1 -1
- data/README.md +53 -85
- data/Rakefile +3 -11
- data/SECURITY.md +17 -0
- data/http.gemspec +15 -6
- data/lib/http/base64.rb +12 -0
- data/lib/http/chainable.rb +71 -41
- data/lib/http/client.rb +73 -52
- data/lib/http/connection.rb +28 -18
- data/lib/http/content_type.rb +12 -7
- data/lib/http/errors.rb +19 -0
- data/lib/http/feature.rb +18 -1
- data/lib/http/features/auto_deflate.rb +27 -6
- data/lib/http/features/auto_inflate.rb +32 -6
- data/lib/http/features/instrumentation.rb +69 -0
- data/lib/http/features/logging.rb +53 -0
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/features/raise_error.rb +22 -0
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/headers/normalizer.rb +69 -0
- data/lib/http/headers.rb +72 -49
- data/lib/http/mime_type/adapter.rb +3 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +31 -28
- data/lib/http/redirector.rb +56 -4
- data/lib/http/request/body.rb +31 -0
- data/lib/http/request/writer.rb +29 -9
- data/lib/http/request.rb +76 -41
- data/lib/http/response/body.rb +6 -4
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +78 -26
- data/lib/http/response/status.rb +4 -3
- data/lib/http/response.rb +45 -27
- data/lib/http/retriable/client.rb +37 -0
- data/lib/http/retriable/delay_calculator.rb +64 -0
- data/lib/http/retriable/errors.rb +14 -0
- data/lib/http/retriable/performer.rb +153 -0
- data/lib/http/timeout/global.rb +29 -47
- data/lib/http/timeout/null.rb +12 -8
- data/lib/http/timeout/per_operation.rb +32 -57
- data/lib/http/uri.rb +75 -1
- data/lib/http/version.rb +1 -1
- data/lib/http.rb +2 -2
- data/spec/lib/http/client_spec.rb +189 -36
- data/spec/lib/http/connection_spec.rb +31 -6
- data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
- data/spec/lib/http/features/instrumentation_spec.rb +81 -0
- data/spec/lib/http/features/logging_spec.rb +65 -0
- data/spec/lib/http/features/raise_error_spec.rb +62 -0
- data/spec/lib/http/headers/normalizer_spec.rb +52 -0
- data/spec/lib/http/headers_spec.rb +53 -18
- data/spec/lib/http/options/headers_spec.rb +6 -2
- data/spec/lib/http/options/merge_spec.rb +16 -16
- data/spec/lib/http/redirector_spec.rb +147 -3
- data/spec/lib/http/request/body_spec.rb +71 -4
- data/spec/lib/http/request/writer_spec.rb +45 -2
- data/spec/lib/http/request_spec.rb +11 -5
- data/spec/lib/http/response/body_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +74 -0
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +83 -7
- data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
- data/spec/lib/http/retriable/performer_spec.rb +302 -0
- data/spec/lib/http/uri/normalizer_spec.rb +95 -0
- data/spec/lib/http/uri_spec.rb +39 -0
- data/spec/lib/http_spec.rb +121 -68
- data/spec/regression_specs.rb +7 -0
- data/spec/spec_helper.rb +22 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server/servlet.rb +42 -11
- data/spec/support/dummy_server.rb +9 -8
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/http_handling_shared.rb +62 -66
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +66 -27
- data/.coveralls.yml +0 -1
- data/.ruby-version +0 -1
- data/.travis.yml +0 -36
|
@@ -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/uri_spec.rb
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
RSpec.describe HTTP::URI do
|
|
4
|
+
let(:example_ipv6_address) { "2606:2800:220:1:248:1893:25c8:1946" }
|
|
5
|
+
|
|
4
6
|
let(:example_http_uri_string) { "http://example.com" }
|
|
5
7
|
let(:example_https_uri_string) { "https://example.com" }
|
|
8
|
+
let(:example_ipv6_uri_string) { "https://[#{example_ipv6_address}]" }
|
|
6
9
|
|
|
7
10
|
subject(:http_uri) { described_class.parse(example_http_uri_string) }
|
|
8
11
|
subject(:https_uri) { described_class.parse(example_https_uri_string) }
|
|
12
|
+
subject(:ipv6_uri) { described_class.parse(example_ipv6_uri_string) }
|
|
9
13
|
|
|
10
14
|
it "knows URI schemes" do
|
|
11
15
|
expect(http_uri.scheme).to eq "http"
|
|
@@ -20,6 +24,41 @@ RSpec.describe HTTP::URI do
|
|
|
20
24
|
expect(https_uri.port).to eq 443
|
|
21
25
|
end
|
|
22
26
|
|
|
27
|
+
describe "#host" do
|
|
28
|
+
it "strips brackets from IPv6 addresses" do
|
|
29
|
+
expect(ipv6_uri.host).to eq("2606:2800:220:1:248:1893:25c8:1946")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe "#normalized_host" do
|
|
34
|
+
it "strips brackets from IPv6 addresses" do
|
|
35
|
+
expect(ipv6_uri.normalized_host).to eq("2606:2800:220:1:248:1893:25c8:1946")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe "#host=" do
|
|
40
|
+
it "updates cached values for #host and #normalized_host" do
|
|
41
|
+
expect(http_uri.host).to eq("example.com")
|
|
42
|
+
expect(http_uri.normalized_host).to eq("example.com")
|
|
43
|
+
|
|
44
|
+
http_uri.host = "[#{example_ipv6_address}]"
|
|
45
|
+
|
|
46
|
+
expect(http_uri.host).to eq(example_ipv6_address)
|
|
47
|
+
expect(http_uri.normalized_host).to eq(example_ipv6_address)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "ensures IPv6 addresses are bracketed in the inner Addressable::URI" do
|
|
51
|
+
expect(http_uri.host).to eq("example.com")
|
|
52
|
+
expect(http_uri.normalized_host).to eq("example.com")
|
|
53
|
+
|
|
54
|
+
http_uri.host = example_ipv6_address
|
|
55
|
+
|
|
56
|
+
expect(http_uri.host).to eq(example_ipv6_address)
|
|
57
|
+
expect(http_uri.normalized_host).to eq(example_ipv6_address)
|
|
58
|
+
expect(http_uri.instance_variable_get(:@uri).host).to eq("[#{example_ipv6_address}]")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
23
62
|
describe "#dup" do
|
|
24
63
|
it "doesn't share internal value between duplicates" do
|
|
25
64
|
duplicated_uri = http_uri.dup
|
data/spec/lib/http_spec.rb
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
1
|
# encoding: utf-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
@@ -55,38 +55,17 @@ RSpec.describe HTTP do
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
context "with a large request body" do
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
it "returns a large body" do
|
|
71
|
-
response = client.post("#{dummy.endpoint}/echo-body", :body => request_body)
|
|
72
|
-
|
|
73
|
-
expect(response.body.to_s).to eq(request_body)
|
|
74
|
-
expect(response.headers["Content-Length"].to_i).to eq(request_body.bytesize)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
context "when bytesize != length" do
|
|
78
|
-
let(:characters) { ("A".."Z").to_a.push("“") }
|
|
79
|
-
|
|
80
|
-
it "returns a large body" do
|
|
81
|
-
body = {:data => request_body}
|
|
82
|
-
response = client.post("#{dummy.endpoint}/echo-body", :json => body)
|
|
83
|
-
|
|
84
|
-
expect(CGI.unescape(response.body.to_s)).to eq(body.to_json)
|
|
85
|
-
expect(response.headers["Content-Length"].to_i).to eq(body.to_json.bytesize)
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
58
|
+
let(:request_body) { "“" * 1_000_000 } # use multi-byte character
|
|
59
|
+
|
|
60
|
+
[:null, 6, {:read => 2, :write => 2, :connect => 2}].each do |timeout|
|
|
61
|
+
context "with `.timeout(#{timeout.inspect})`" do
|
|
62
|
+
let(:client) { HTTP.timeout(timeout) }
|
|
63
|
+
|
|
64
|
+
it "writes the whole body" do
|
|
65
|
+
response = client.post "#{dummy.endpoint}/echo-body", :body => request_body
|
|
66
|
+
|
|
67
|
+
expect(response.body.to_s).to eq(request_body.b)
|
|
68
|
+
expect(response.headers["Content-Length"].to_i).to eq request_body.bytesize
|
|
90
69
|
end
|
|
91
70
|
end
|
|
92
71
|
end
|
|
@@ -116,7 +95,8 @@ RSpec.describe HTTP do
|
|
|
116
95
|
expect(response.to_s).to match(/<!doctype html>/)
|
|
117
96
|
end
|
|
118
97
|
|
|
119
|
-
|
|
98
|
+
# TODO: htt:s://github.com/httprb/http/issues/627
|
|
99
|
+
xcontext "ssl" do
|
|
120
100
|
it "responds with the endpoint's body" do
|
|
121
101
|
response = ssl_client.via(proxy.addr, proxy.port).get dummy_ssl.endpoint
|
|
122
102
|
expect(response.to_s).to match(/<!doctype html>/)
|
|
@@ -152,7 +132,8 @@ RSpec.describe HTTP do
|
|
|
152
132
|
expect(response.status).to eq(407)
|
|
153
133
|
end
|
|
154
134
|
|
|
155
|
-
|
|
135
|
+
# TODO: htt:s://github.com/httprb/http/issues/627
|
|
136
|
+
xcontext "ssl" do
|
|
156
137
|
it "responds with the endpoint's body" do
|
|
157
138
|
response = ssl_client.via(proxy.addr, proxy.port, "username", "password").get dummy_ssl.endpoint
|
|
158
139
|
expect(response.to_s).to match(/<!doctype html>/)
|
|
@@ -171,6 +152,35 @@ RSpec.describe HTTP do
|
|
|
171
152
|
end
|
|
172
153
|
end
|
|
173
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
|
+
|
|
174
184
|
context "posting forms to resources" do
|
|
175
185
|
it "is easy" do
|
|
176
186
|
response = HTTP.post "#{dummy.endpoint}/form", :form => {:example => "testing-form"}
|
|
@@ -194,7 +204,7 @@ RSpec.describe HTTP do
|
|
|
194
204
|
|
|
195
205
|
context "with encoding option" do
|
|
196
206
|
it "respects option" do
|
|
197
|
-
response = HTTP.get "#{dummy.endpoint}/iso-8859-1",
|
|
207
|
+
response = HTTP.get "#{dummy.endpoint}/iso-8859-1", :encoding => Encoding::BINARY
|
|
198
208
|
expect(response.to_s.encoding).to eq(Encoding::BINARY)
|
|
199
209
|
end
|
|
200
210
|
end
|
|
@@ -202,7 +212,7 @@ RSpec.describe HTTP do
|
|
|
202
212
|
|
|
203
213
|
context "passing a string encoding type" do
|
|
204
214
|
it "finds encoding" do
|
|
205
|
-
response = HTTP.get dummy.endpoint,
|
|
215
|
+
response = HTTP.get dummy.endpoint, :encoding => "ascii"
|
|
206
216
|
expect(response.to_s.encoding).to eq(Encoding::ASCII)
|
|
207
217
|
end
|
|
208
218
|
end
|
|
@@ -255,15 +265,15 @@ RSpec.describe HTTP do
|
|
|
255
265
|
|
|
256
266
|
describe ".basic_auth" do
|
|
257
267
|
it "fails when options is not a Hash" do
|
|
258
|
-
expect { HTTP.basic_auth "[FOOBAR]" }.to raise_error
|
|
268
|
+
expect { HTTP.basic_auth "[FOOBAR]" }.to raise_error(NoMethodError)
|
|
259
269
|
end
|
|
260
270
|
|
|
261
271
|
it "fails when :pass is not given" do
|
|
262
|
-
expect { HTTP.basic_auth :user => "[USER]" }.to raise_error
|
|
272
|
+
expect { HTTP.basic_auth :user => "[USER]" }.to raise_error(KeyError)
|
|
263
273
|
end
|
|
264
274
|
|
|
265
275
|
it "fails when :user is not given" do
|
|
266
|
-
expect { HTTP.basic_auth :pass => "[PASS]" }.to raise_error
|
|
276
|
+
expect { HTTP.basic_auth :pass => "[PASS]" }.to raise_error(KeyError)
|
|
267
277
|
end
|
|
268
278
|
|
|
269
279
|
it "sets Authorization header with proper BasicAuth value" do
|
|
@@ -305,22 +315,8 @@ RSpec.describe HTTP do
|
|
|
305
315
|
end
|
|
306
316
|
|
|
307
317
|
describe ".timeout" do
|
|
308
|
-
context "
|
|
309
|
-
subject(:client) { HTTP.timeout :
|
|
310
|
-
|
|
311
|
-
it "sets timeout_class to PerOperation" do
|
|
312
|
-
expect(client.default_options.timeout_class).
|
|
313
|
-
to be HTTP::Timeout::PerOperation
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
it "sets given timeout options" do
|
|
317
|
-
expect(client.default_options.timeout_options).
|
|
318
|
-
to eq :read_timeout => 123
|
|
319
|
-
end
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
context "with :null type" do
|
|
323
|
-
subject(:client) { HTTP.timeout :null, :read => 123 }
|
|
318
|
+
context "specifying a null timeout" do
|
|
319
|
+
subject(:client) { HTTP.timeout :null }
|
|
324
320
|
|
|
325
321
|
it "sets timeout_class to Null" do
|
|
326
322
|
expect(client.default_options.timeout_class).
|
|
@@ -328,8 +324,8 @@ RSpec.describe HTTP do
|
|
|
328
324
|
end
|
|
329
325
|
end
|
|
330
326
|
|
|
331
|
-
context "
|
|
332
|
-
subject(:client) { HTTP.timeout :
|
|
327
|
+
context "specifying per operation timeouts" do
|
|
328
|
+
subject(:client) { HTTP.timeout :read => 123 }
|
|
333
329
|
|
|
334
330
|
it "sets timeout_class to PerOperation" do
|
|
335
331
|
expect(client.default_options.timeout_class).
|
|
@@ -342,24 +338,28 @@ RSpec.describe HTTP do
|
|
|
342
338
|
end
|
|
343
339
|
end
|
|
344
340
|
|
|
345
|
-
context "
|
|
346
|
-
|
|
341
|
+
context "specifying per operation timeouts as frozen hash" do
|
|
342
|
+
let(:frozen_options) { {:read => 123}.freeze }
|
|
343
|
+
subject(:client) { HTTP.timeout(frozen_options) }
|
|
344
|
+
|
|
345
|
+
it "does not raise an error" do
|
|
346
|
+
expect { client }.not_to raise_error
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
context "specifying a global timeout" do
|
|
351
|
+
subject(:client) { HTTP.timeout 123 }
|
|
347
352
|
|
|
348
353
|
it "sets timeout_class to Global" do
|
|
349
354
|
expect(client.default_options.timeout_class).
|
|
350
355
|
to be HTTP::Timeout::Global
|
|
351
356
|
end
|
|
352
357
|
|
|
353
|
-
it "sets given timeout
|
|
358
|
+
it "sets given timeout option" do
|
|
354
359
|
expect(client.default_options.timeout_options).
|
|
355
|
-
to eq :
|
|
360
|
+
to eq :global_timeout => 123
|
|
356
361
|
end
|
|
357
362
|
end
|
|
358
|
-
|
|
359
|
-
it "fails with unknown timeout type" do
|
|
360
|
-
expect { HTTP.timeout(:foobar, :read => 123) }.
|
|
361
|
-
to raise_error(ArgumentError, /foobar/)
|
|
362
|
-
end
|
|
363
363
|
end
|
|
364
364
|
|
|
365
365
|
describe ".cookies" do
|
|
@@ -469,6 +469,59 @@ RSpec.describe HTTP do
|
|
|
469
469
|
|
|
470
470
|
expect(response.to_s).to eq("#{body}-deflated")
|
|
471
471
|
end
|
|
472
|
+
|
|
473
|
+
it "returns empty body for no content response where Content-Encoding is gzip" do
|
|
474
|
+
client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "gzip")
|
|
475
|
+
body = "Hello!"
|
|
476
|
+
response = client.post("#{dummy.endpoint}/no-content-204", :body => body)
|
|
477
|
+
|
|
478
|
+
expect(response.to_s).to eq("")
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
it "returns empty body for no content response where Content-Encoding is deflate" do
|
|
482
|
+
client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "deflate")
|
|
483
|
+
body = "Hello!"
|
|
484
|
+
response = client.post("#{dummy.endpoint}/no-content-204", :body => body)
|
|
485
|
+
|
|
486
|
+
expect(response.to_s).to eq("")
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
context "with :normalize_uri" do
|
|
491
|
+
it "normalizes URI" do
|
|
492
|
+
response = HTTP.get "#{dummy.endpoint}/héllö-wörld"
|
|
493
|
+
expect(response.to_s).to eq("hello world")
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
it "uses the custom URI Normalizer method" do
|
|
497
|
+
client = HTTP.use(:normalize_uri => {:normalizer => :itself.to_proc})
|
|
498
|
+
response = client.get("#{dummy.endpoint}/héllö-wörld")
|
|
499
|
+
expect(response.status).to eq(400)
|
|
500
|
+
end
|
|
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
|
+
|
|
519
|
+
it "uses the default URI normalizer" do
|
|
520
|
+
client = HTTP.use :normalize_uri
|
|
521
|
+
expect(HTTP::URI::NORMALIZER).to receive(:call).and_call_original
|
|
522
|
+
response = client.get("#{dummy.endpoint}/héllö-wörld")
|
|
523
|
+
expect(response.to_s).to eq("hello world")
|
|
524
|
+
end
|
|
472
525
|
end
|
|
473
526
|
end
|
|
474
527
|
|
|
@@ -476,7 +529,7 @@ RSpec.describe HTTP do
|
|
|
476
529
|
expect { HTTP.get "http://thishostshouldnotexists.com" }.
|
|
477
530
|
to raise_error HTTP::ConnectionError
|
|
478
531
|
|
|
479
|
-
expect { HTTP.get "http://127.0.0.1:
|
|
532
|
+
expect { HTTP.get "http://127.0.0.1:111" }.
|
|
480
533
|
to raise_error HTTP::ConnectionError
|
|
481
534
|
end
|
|
482
535
|
end
|
data/spec/regression_specs.rb
CHANGED
|
@@ -14,4 +14,11 @@ RSpec.describe "Regression testing" do
|
|
|
14
14
|
expect { HTTP.get(google_uri).to_s }.not_to raise_error
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
|
+
|
|
18
|
+
describe "#422" do
|
|
19
|
+
it "reads body when 200 OK response contains Upgrade header" do
|
|
20
|
+
res = HTTP.get("https://httpbin.org/response-headers?Upgrade=h2,h2c")
|
|
21
|
+
expect(res.parse(:json)).to include("Upgrade" => "h2,h2c")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
17
24
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,22 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new(
|
|
7
|
-
[
|
|
8
|
-
SimpleCov::Formatter::HTMLFormatter,
|
|
9
|
-
Coveralls::SimpleCov::Formatter
|
|
10
|
-
]
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
SimpleCov.start do
|
|
14
|
-
add_filter "/spec/"
|
|
15
|
-
minimum_coverage 80
|
|
16
|
-
end
|
|
3
|
+
require_relative "./support/simplecov"
|
|
4
|
+
require_relative "./support/fuubar" unless ENV["CI"]
|
|
17
5
|
|
|
18
6
|
require "http"
|
|
19
7
|
require "rspec/its"
|
|
8
|
+
require "rspec/memory"
|
|
20
9
|
require "support/capture_warning"
|
|
21
10
|
require "support/fakeio"
|
|
22
11
|
|
|
@@ -40,6 +29,13 @@ RSpec.configure do |config|
|
|
|
40
29
|
mocks.verify_partial_doubles = true
|
|
41
30
|
end
|
|
42
31
|
|
|
32
|
+
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
|
|
33
|
+
# have no way to turn it off -- the option exists only for backwards
|
|
34
|
+
# compatibility in RSpec 3). It causes shared context metadata to be
|
|
35
|
+
# inherited by the metadata hash of host groups and examples, rather than
|
|
36
|
+
# triggering implicit auto-inclusion in groups with matching metadata.
|
|
37
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
|
38
|
+
|
|
43
39
|
# These two settings work together to allow you to limit a spec run
|
|
44
40
|
# to individual examples or groups you care about by tagging them with
|
|
45
41
|
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
|
@@ -48,17 +44,22 @@ RSpec.configure do |config|
|
|
|
48
44
|
config.filter_run_excluding :flaky if defined?(JRUBY_VERSION) && ENV["CI"]
|
|
49
45
|
config.run_all_when_everything_filtered = true
|
|
50
46
|
|
|
51
|
-
# Limits the available syntax to the non-monkey patched syntax that is recommended.
|
|
52
|
-
# For more details, see:
|
|
53
|
-
# - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
|
|
54
|
-
# - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
|
55
|
-
# - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
|
|
56
|
-
config.disable_monkey_patching!
|
|
57
|
-
|
|
58
47
|
# This setting enables warnings. It's recommended, but in some cases may
|
|
59
48
|
# be too noisy due to issues in dependencies.
|
|
60
49
|
config.warnings = 0 == ENV["GUARD_RSPEC"].to_i
|
|
61
50
|
|
|
51
|
+
# Allows RSpec to persist some state between runs in order to support
|
|
52
|
+
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
|
53
|
+
# you configure your source control system to ignore this file.
|
|
54
|
+
config.example_status_persistence_file_path = "spec/examples.txt"
|
|
55
|
+
|
|
56
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
|
57
|
+
# recommended. For more details, see:
|
|
58
|
+
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
|
|
59
|
+
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
|
60
|
+
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
|
|
61
|
+
config.disable_monkey_patching!
|
|
62
|
+
|
|
62
63
|
# Many RSpec users commonly either run the entire suite or an individual
|
|
63
64
|
# file, and it's useful to allow more verbose output when running an
|
|
64
65
|
# individual spec file.
|
data/spec/support/black_hole.rb
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
1
|
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "cgi"
|
|
3
5
|
|
|
4
6
|
class DummyServer < WEBrick::HTTPServer
|
|
5
|
-
# rubocop:disable Metrics/ClassLength
|
|
6
|
-
class Servlet < WEBrick::HTTPServlet::AbstractServlet
|
|
7
|
+
class Servlet < WEBrick::HTTPServlet::AbstractServlet # rubocop:disable Metrics/ClassLength
|
|
7
8
|
def self.sockets
|
|
8
9
|
@sockets ||= []
|
|
9
10
|
end
|
|
10
11
|
|
|
11
|
-
def not_found(
|
|
12
|
+
def not_found(req, res)
|
|
12
13
|
res.status = 404
|
|
13
|
-
res.body = "
|
|
14
|
+
res.body = "#{req.unparsed_uri} not found"
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def self.handlers
|
|
17
18
|
@handlers ||= {}
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
def initialize(server, memo)
|
|
22
|
+
super(server)
|
|
23
|
+
@memo = memo
|
|
24
|
+
end
|
|
25
|
+
|
|
20
26
|
%w[get post head].each do |method|
|
|
21
|
-
class_eval <<-RUBY, __FILE__, __LINE__
|
|
27
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
22
28
|
def self.#{method}(path, &block)
|
|
23
29
|
handlers["#{method}:\#{path}"] = block
|
|
24
30
|
end
|
|
@@ -26,7 +32,7 @@ class DummyServer < WEBrick::HTTPServer
|
|
|
26
32
|
def do_#{method.upcase}(req, res)
|
|
27
33
|
handler = self.class.handlers["#{method}:\#{req.path}"]
|
|
28
34
|
return instance_exec(req, res, &handler) if handler
|
|
29
|
-
not_found
|
|
35
|
+
not_found(req, res)
|
|
30
36
|
end
|
|
31
37
|
RUBY
|
|
32
38
|
end
|
|
@@ -67,7 +73,7 @@ class DummyServer < WEBrick::HTTPServer
|
|
|
67
73
|
end
|
|
68
74
|
|
|
69
75
|
get "/params" do |req, res|
|
|
70
|
-
next not_found unless "foo=bar" == req.query_string
|
|
76
|
+
next not_found(req, res) unless "foo=bar" == req.query_string
|
|
71
77
|
|
|
72
78
|
res.status = 200
|
|
73
79
|
res.body = "Params!"
|
|
@@ -76,7 +82,7 @@ class DummyServer < WEBrick::HTTPServer
|
|
|
76
82
|
get "/multiple-params" do |req, res|
|
|
77
83
|
params = CGI.parse req.query_string
|
|
78
84
|
|
|
79
|
-
next not_found unless {"foo" => ["bar"], "baz" => ["quux"]} == params
|
|
85
|
+
next not_found(req, res) unless {"foo" => ["bar"], "baz" => ["quux"]} == params
|
|
80
86
|
|
|
81
87
|
res.status = 200
|
|
82
88
|
res.body = "More Params!"
|
|
@@ -148,11 +154,16 @@ class DummyServer < WEBrick::HTTPServer
|
|
|
148
154
|
res.body = req.body
|
|
149
155
|
end
|
|
150
156
|
|
|
157
|
+
get "/héllö-wörld".b do |_req, res|
|
|
158
|
+
res.status = 200
|
|
159
|
+
res.body = "hello world"
|
|
160
|
+
end
|
|
161
|
+
|
|
151
162
|
post "/encoded-body" do |req, res|
|
|
152
163
|
res.status = 200
|
|
153
164
|
|
|
154
165
|
res.body = case req["Accept-Encoding"]
|
|
155
|
-
when "gzip"
|
|
166
|
+
when "gzip"
|
|
156
167
|
res["Content-Encoding"] = "gzip"
|
|
157
168
|
StringIO.open do |out|
|
|
158
169
|
Zlib::GzipWriter.wrap(out) do |gz|
|
|
@@ -161,12 +172,32 @@ class DummyServer < WEBrick::HTTPServer
|
|
|
161
172
|
out.tap(&:rewind).read
|
|
162
173
|
end
|
|
163
174
|
end
|
|
164
|
-
when "deflate"
|
|
175
|
+
when "deflate"
|
|
165
176
|
res["Content-Encoding"] = "deflate"
|
|
166
177
|
Zlib::Deflate.deflate("#{req.body}-deflated")
|
|
167
178
|
else
|
|
168
179
|
"#{req.body}-raw"
|
|
169
180
|
end
|
|
170
181
|
end
|
|
182
|
+
|
|
183
|
+
post "/no-content-204" do |req, res|
|
|
184
|
+
res.status = 204
|
|
185
|
+
res.body = ""
|
|
186
|
+
|
|
187
|
+
case req["Accept-Encoding"]
|
|
188
|
+
when "gzip"
|
|
189
|
+
res["Content-Encoding"] = "gzip"
|
|
190
|
+
when "deflate"
|
|
191
|
+
res["Content-Encoding"] = "deflate"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
get "/retry-2" do |_req, res|
|
|
196
|
+
@memo[:attempts] ||= 0
|
|
197
|
+
@memo[:attempts] += 1
|
|
198
|
+
|
|
199
|
+
res.body = "retried #{@memo[:attempts]}x"
|
|
200
|
+
res.status = @memo[:attempts] == 2 ? 200 : 500
|
|
201
|
+
end
|
|
171
202
|
end
|
|
172
203
|
end
|
|
@@ -13,20 +13,21 @@ class DummyServer < WEBrick::HTTPServer
|
|
|
13
13
|
include ServerConfig
|
|
14
14
|
|
|
15
15
|
CONFIG = {
|
|
16
|
-
:BindAddress
|
|
17
|
-
:Port
|
|
18
|
-
:AccessLog
|
|
19
|
-
:Logger
|
|
16
|
+
:BindAddress => "127.0.0.1",
|
|
17
|
+
:Port => 0,
|
|
18
|
+
:AccessLog => BlackHole,
|
|
19
|
+
:Logger => BlackHole
|
|
20
20
|
}.freeze
|
|
21
21
|
|
|
22
22
|
SSL_CONFIG = CONFIG.merge(
|
|
23
|
-
:SSLEnable
|
|
24
|
-
:SSLStartImmediately
|
|
23
|
+
:SSLEnable => true,
|
|
24
|
+
:SSLStartImmediately => true
|
|
25
25
|
).freeze
|
|
26
26
|
|
|
27
|
-
def initialize(options = {})
|
|
27
|
+
def initialize(options = {})
|
|
28
28
|
super(options[:ssl] ? SSL_CONFIG : CONFIG)
|
|
29
|
-
|
|
29
|
+
@memo = {}
|
|
30
|
+
mount("/", Servlet, @memo)
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
def endpoint
|