http 4.4.1 → 5.1.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 +65 -0
- data/.gitignore +6 -10
- data/.rspec +0 -4
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop.yml +8 -110
- data/.rubocop_todo.yml +205 -0
- data/.yardopts +1 -1
- data/CHANGES.md +188 -3
- data/Gemfile +18 -10
- data/LICENSE.txt +1 -1
- data/README.md +47 -86
- data/Rakefile +2 -10
- data/SECURITY.md +5 -0
- data/http.gemspec +9 -8
- data/lib/http/chainable.rb +23 -17
- data/lib/http/client.rb +44 -34
- data/lib/http/connection.rb +11 -7
- data/lib/http/content_type.rb +12 -7
- data/lib/http/errors.rb +3 -0
- data/lib/http/feature.rb +3 -1
- data/lib/http/features/auto_deflate.rb +6 -6
- data/lib/http/features/auto_inflate.rb +6 -7
- data/lib/http/features/instrumentation.rb +1 -1
- data/lib/http/features/logging.rb +19 -21
- data/lib/http/headers.rb +50 -13
- data/lib/http/mime_type/adapter.rb +3 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +5 -8
- data/lib/http/redirector.rb +51 -2
- data/lib/http/request/body.rb +1 -0
- data/lib/http/request/writer.rb +9 -4
- data/lib/http/request.rb +28 -11
- data/lib/http/response/body.rb +6 -4
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +74 -62
- data/lib/http/response/status.rb +4 -3
- data/lib/http/response.rb +44 -18
- data/lib/http/timeout/global.rb +20 -36
- data/lib/http/timeout/null.rb +2 -1
- data/lib/http/timeout/per_operation.rb +32 -55
- data/lib/http/uri.rb +5 -5
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +153 -30
- data/spec/lib/http/connection_spec.rb +8 -5
- data/spec/lib/http/features/auto_inflate_spec.rb +3 -2
- data/spec/lib/http/features/instrumentation_spec.rb +27 -21
- data/spec/lib/http/features/logging_spec.rb +8 -10
- data/spec/lib/http/headers_spec.rb +53 -18
- data/spec/lib/http/options/headers_spec.rb +1 -1
- data/spec/lib/http/options/merge_spec.rb +16 -16
- data/spec/lib/http/redirector_spec.rb +107 -3
- data/spec/lib/http/request/body_spec.rb +3 -3
- data/spec/lib/http/request/writer_spec.rb +25 -2
- data/spec/lib/http/request_spec.rb +5 -5
- data/spec/lib/http/response/body_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +33 -4
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +80 -3
- data/spec/lib/http_spec.rb +30 -3
- data/spec/spec_helper.rb +21 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server/servlet.rb +17 -6
- data/spec/support/dummy_server.rb +7 -7
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/http_handling_shared.rb +5 -5
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +23 -15
- data/.coveralls.yml +0 -1
- data/.travis.yml +0 -39
@@ -6,12 +6,17 @@ RSpec.describe HTTP::Redirector do
|
|
6
6
|
:status => status,
|
7
7
|
:version => "1.1",
|
8
8
|
:headers => headers,
|
9
|
-
:body => body
|
9
|
+
:body => body,
|
10
|
+
:request => HTTP::Request.new(:verb => :get, :uri => "http://example.com")
|
10
11
|
)
|
11
12
|
end
|
12
13
|
|
13
|
-
def redirect_response(status, location)
|
14
|
-
simple_response status, "", "Location" => location
|
14
|
+
def redirect_response(status, location, set_cookie = {})
|
15
|
+
res = simple_response status, "", "Location" => location
|
16
|
+
set_cookie.each do |name, value|
|
17
|
+
res.headers.add("Set-Cookie", "#{name}=#{value}; path=/; httponly; secure; SameSite=none; Secure")
|
18
|
+
end
|
19
|
+
res
|
15
20
|
end
|
16
21
|
|
17
22
|
describe "#strict" do
|
@@ -88,6 +93,61 @@ RSpec.describe HTTP::Redirector do
|
|
88
93
|
expect(res.to_s).to eq "http://example.com/123"
|
89
94
|
end
|
90
95
|
|
96
|
+
it "returns cookies in response" do
|
97
|
+
req = HTTP::Request.new :verb => :head, :uri => "http://example.com"
|
98
|
+
hops = [
|
99
|
+
redirect_response(301, "http://example.com/1", {"foo" => "42"}),
|
100
|
+
redirect_response(301, "http://example.com/2", {"bar" => "53", "deleted" => "foo"}),
|
101
|
+
redirect_response(301, "http://example.com/3", {"baz" => "64", "deleted" => ""}),
|
102
|
+
redirect_response(301, "http://example.com/4", {"baz" => "65"}),
|
103
|
+
simple_response(200, "bar")
|
104
|
+
]
|
105
|
+
|
106
|
+
request_cookies = [
|
107
|
+
{"foo" => "42"},
|
108
|
+
{"foo" => "42", "bar" => "53", "deleted" => "foo"},
|
109
|
+
{"foo" => "42", "bar" => "53", "baz" => "64"},
|
110
|
+
{"foo" => "42", "bar" => "53", "baz" => "65"}
|
111
|
+
]
|
112
|
+
|
113
|
+
res = redirector.perform(req, hops.shift) do |request|
|
114
|
+
req_cookie = HTTP::Cookie.cookie_value_to_hash(request.headers["Cookie"] || "")
|
115
|
+
expect(req_cookie).to eq request_cookies.shift
|
116
|
+
hops.shift
|
117
|
+
end
|
118
|
+
expect(res.to_s).to eq "bar"
|
119
|
+
cookies = res.cookies.cookies.to_h { |c| [c.name, c.value] }
|
120
|
+
expect(cookies["foo"]).to eq "42"
|
121
|
+
expect(cookies["bar"]).to eq "53"
|
122
|
+
expect(cookies["baz"]).to eq "65"
|
123
|
+
expect(cookies["deleted"]).to eq nil
|
124
|
+
end
|
125
|
+
|
126
|
+
it "returns original cookies in response" do
|
127
|
+
req = HTTP::Request.new :verb => :head, :uri => "http://example.com"
|
128
|
+
req.headers.set("Cookie", "foo=42; deleted=baz")
|
129
|
+
hops = [
|
130
|
+
redirect_response(301, "http://example.com/1", {"bar" => "64", "deleted" => ""}),
|
131
|
+
simple_response(200, "bar")
|
132
|
+
]
|
133
|
+
|
134
|
+
request_cookies = [
|
135
|
+
{"foo" => "42", "bar" => "64"},
|
136
|
+
{"foo" => "42", "bar" => "64"}
|
137
|
+
]
|
138
|
+
|
139
|
+
res = redirector.perform(req, hops.shift) do |request|
|
140
|
+
req_cookie = HTTP::Cookie.cookie_value_to_hash(request.headers["Cookie"] || "")
|
141
|
+
expect(req_cookie).to eq request_cookies.shift
|
142
|
+
hops.shift
|
143
|
+
end
|
144
|
+
expect(res.to_s).to eq "bar"
|
145
|
+
cookies = res.cookies.cookies.to_h { |c| [c.name, c.value] }
|
146
|
+
expect(cookies["foo"]).to eq "42"
|
147
|
+
expect(cookies["bar"]).to eq "64"
|
148
|
+
expect(cookies["deleted"]).to eq nil
|
149
|
+
end
|
150
|
+
|
91
151
|
context "following 300 redirect" do
|
92
152
|
context "with strict mode" do
|
93
153
|
let(:options) { {:strict => true} }
|
@@ -395,5 +455,49 @@ RSpec.describe HTTP::Redirector do
|
|
395
455
|
end
|
396
456
|
end
|
397
457
|
end
|
458
|
+
|
459
|
+
describe "changing verbs during redirects" do
|
460
|
+
let(:options) { {:strict => false} }
|
461
|
+
let(:post_body) { HTTP::Request::Body.new("i might be way longer in real life") }
|
462
|
+
let(:cookie) { "dont=eat my cookies" }
|
463
|
+
|
464
|
+
def a_dangerous_request(verb)
|
465
|
+
HTTP::Request.new(
|
466
|
+
:verb => verb, :uri => "http://example.com",
|
467
|
+
:body => post_body, :headers => {
|
468
|
+
"Content-Type" => "meme",
|
469
|
+
"Cookie" => cookie
|
470
|
+
}
|
471
|
+
)
|
472
|
+
end
|
473
|
+
|
474
|
+
def empty_body
|
475
|
+
HTTP::Request::Body.new(nil)
|
476
|
+
end
|
477
|
+
|
478
|
+
it "follows without body/content type if it has to change verb" do
|
479
|
+
req = a_dangerous_request(:post)
|
480
|
+
res = redirect_response 302, "http://example.com/1"
|
481
|
+
|
482
|
+
redirector.perform(req, res) do |prev_req, _|
|
483
|
+
expect(prev_req.body).to eq(empty_body)
|
484
|
+
expect(prev_req.headers["Cookie"]).to eq(cookie)
|
485
|
+
expect(prev_req.headers["Content-Type"]).to eq(nil)
|
486
|
+
simple_response 200
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
it "leaves body/content-type intact if it does not have to change verb" do
|
491
|
+
req = a_dangerous_request(:post)
|
492
|
+
res = redirect_response 307, "http://example.com/1"
|
493
|
+
|
494
|
+
redirector.perform(req, res) do |prev_req, _|
|
495
|
+
expect(prev_req.body).to eq(post_body)
|
496
|
+
expect(prev_req.headers["Cookie"]).to eq(cookie)
|
497
|
+
expect(prev_req.headers["Content-Type"]).to eq("meme")
|
498
|
+
simple_response 200
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
398
502
|
end
|
399
503
|
end
|
@@ -118,10 +118,10 @@ RSpec.describe HTTP::Request::Body do
|
|
118
118
|
end
|
119
119
|
|
120
120
|
context "when body is a non-Enumerable IO" do
|
121
|
-
let(:body) { FakeIO.new("a" * 16 * 1024 + "b" * 10 * 1024) }
|
121
|
+
let(:body) { FakeIO.new(("a" * 16 * 1024) + ("b" * 10 * 1024)) }
|
122
122
|
|
123
123
|
it "yields chunks of content" do
|
124
|
-
expect(chunks.inject("", :+)).to eq "a" * 16 * 1024 + "b" * 10 * 1024
|
124
|
+
expect(chunks.inject("", :+)).to eq ("a" * 16 * 1024) + ("b" * 10 * 1024)
|
125
125
|
end
|
126
126
|
end
|
127
127
|
|
@@ -148,7 +148,7 @@ RSpec.describe HTTP::Request::Body do
|
|
148
148
|
end
|
149
149
|
|
150
150
|
context "when body is an Enumerable IO" do
|
151
|
-
let(:data) { "a" * 16 * 1024 + "b" * 10 * 1024 }
|
151
|
+
let(:data) { ("a" * 16 * 1024) + ("b" * 10 * 1024) }
|
152
152
|
let(:body) { StringIO.new data }
|
153
153
|
|
154
154
|
it "yields chunks of content" do
|
@@ -1,5 +1,5 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
1
|
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
3
|
|
4
4
|
RSpec.describe HTTP::Request::Writer do
|
5
5
|
let(:io) { StringIO.new }
|
@@ -22,6 +22,18 @@ RSpec.describe HTTP::Request::Writer do
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
+
context "when headers are specified as strings with mixed case" do
|
26
|
+
let(:headers) { HTTP::Headers.coerce "content-Type" => "text", "X_MAX" => "200" }
|
27
|
+
|
28
|
+
it "writes the headers with the same casing" do
|
29
|
+
writer.stream
|
30
|
+
expect(io.string).to eq [
|
31
|
+
"#{headerstart}\r\n",
|
32
|
+
"content-Type: text\r\nX_MAX: 200\r\nContent-Length: 0\r\n\r\n"
|
33
|
+
].join
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
25
37
|
context "when body is nonempty" do
|
26
38
|
let(:body) { HTTP::Request::Body.new("content") }
|
27
39
|
|
@@ -35,9 +47,20 @@ RSpec.describe HTTP::Request::Writer do
|
|
35
47
|
end
|
36
48
|
end
|
37
49
|
|
38
|
-
context "when body is
|
50
|
+
context "when body is not set" do
|
39
51
|
let(:body) { HTTP::Request::Body.new(nil) }
|
40
52
|
|
53
|
+
it "doesn't write anything to the socket and doesn't set Content-Length" do
|
54
|
+
writer.stream
|
55
|
+
expect(io.string).to eq [
|
56
|
+
"#{headerstart}\r\n\r\n"
|
57
|
+
].join
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "when body is empty" do
|
62
|
+
let(:body) { HTTP::Request::Body.new("") }
|
63
|
+
|
41
64
|
it "doesn't write anything to the socket and sets Content-Length" do
|
42
65
|
writer.stream
|
43
66
|
expect(io.string).to eq [
|
@@ -1,5 +1,5 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
1
|
# coding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
3
|
|
4
4
|
RSpec.describe HTTP::Request do
|
5
5
|
let(:proxy) { {} }
|
@@ -8,10 +8,10 @@ RSpec.describe HTTP::Request do
|
|
8
8
|
|
9
9
|
subject :request do
|
10
10
|
HTTP::Request.new(
|
11
|
-
:verb
|
12
|
-
:uri
|
13
|
-
:headers
|
14
|
-
:proxy
|
11
|
+
:verb => :get,
|
12
|
+
:uri => request_uri,
|
13
|
+
:headers => headers,
|
14
|
+
:proxy => proxy
|
15
15
|
)
|
16
16
|
end
|
17
17
|
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
RSpec.describe HTTP::Response::Body do
|
4
4
|
let(:connection) { double(:sequence_id => 0) }
|
5
|
-
let(:chunks) { [
|
5
|
+
let(:chunks) { ["Hello, ", "World!"] }
|
6
6
|
|
7
7
|
before do
|
8
8
|
allow(connection).to receive(:readpartial) { chunks.shift }
|
@@ -16,7 +16,7 @@ RSpec.describe HTTP::Response::Body do
|
|
16
16
|
end
|
17
17
|
|
18
18
|
context "when body empty" do
|
19
|
-
let(:chunks) { [
|
19
|
+
let(:chunks) { [""] }
|
20
20
|
|
21
21
|
it "returns responds to empty? with true" do
|
22
22
|
expect(subject).to be_empty
|
@@ -45,12 +45,12 @@ RSpec.describe HTTP::Response::Body do
|
|
45
45
|
it "returns content in specified encoding" do
|
46
46
|
body = described_class.new(connection)
|
47
47
|
expect(connection).to receive(:readpartial).
|
48
|
-
and_return(String.new("content"
|
48
|
+
and_return(String.new("content", :encoding => Encoding::UTF_8))
|
49
49
|
expect(body.readpartial.encoding).to eq Encoding::BINARY
|
50
50
|
|
51
51
|
body = described_class.new(connection, :encoding => Encoding::UTF_8)
|
52
52
|
expect(connection).to receive(:readpartial).
|
53
|
-
and_return(String.new("content"
|
53
|
+
and_return(String.new("content", :encoding => Encoding::BINARY))
|
54
54
|
expect(body.readpartial.encoding).to eq Encoding::UTF_8
|
55
55
|
end
|
56
56
|
end
|
@@ -59,7 +59,7 @@ RSpec.describe HTTP::Response::Body do
|
|
59
59
|
let(:chunks) do
|
60
60
|
body = Zlib::Deflate.deflate("Hi, HTTP here ☺")
|
61
61
|
len = body.length
|
62
|
-
[
|
62
|
+
[body[0, len / 2], body[(len / 2)..]]
|
63
63
|
end
|
64
64
|
subject(:body) do
|
65
65
|
inflater = HTTP::Response::Inflater.new(connection)
|
@@ -3,14 +3,14 @@
|
|
3
3
|
RSpec.describe HTTP::Response::Parser do
|
4
4
|
subject(:parser) { described_class.new }
|
5
5
|
let(:raw_response) do
|
6
|
-
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: application/json\r\
|
6
|
+
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nContent-Type: application/json\r\nMyHeader: val\r\nEmptyHeader: \r\n\r\n{}"
|
7
7
|
end
|
8
8
|
let(:expected_headers) do
|
9
9
|
{
|
10
10
|
"Content-Length" => "2",
|
11
11
|
"Content-Type" => "application/json",
|
12
|
-
"
|
13
|
-
"
|
12
|
+
"MyHeader" => "val",
|
13
|
+
"EmptyHeader" => ""
|
14
14
|
}
|
15
15
|
end
|
16
16
|
let(:expected_body) { "{}" }
|
@@ -32,7 +32,7 @@ RSpec.describe HTTP::Response::Parser do
|
|
32
32
|
end
|
33
33
|
|
34
34
|
context "response in many parts" do
|
35
|
-
let(:parts) { raw_response.
|
35
|
+
let(:parts) { raw_response.chars }
|
36
36
|
|
37
37
|
it "parses headers" do
|
38
38
|
expect(subject.headers.to_h).to eq(expected_headers)
|
@@ -42,4 +42,33 @@ RSpec.describe HTTP::Response::Parser do
|
|
42
42
|
expect(subject.read(expected_body.size)).to eq(expected_body)
|
43
43
|
end
|
44
44
|
end
|
45
|
+
|
46
|
+
context "when got 100 Continue response" do
|
47
|
+
let :raw_response do
|
48
|
+
"HTTP/1.1 100 Continue\r\n\r\n" \
|
49
|
+
"HTTP/1.1 200 OK\r\n" \
|
50
|
+
"Content-Length: 12\r\n\r\n" \
|
51
|
+
"Hello World!"
|
52
|
+
end
|
53
|
+
|
54
|
+
context "when response is feeded in one part" do
|
55
|
+
let(:parts) { [raw_response] }
|
56
|
+
|
57
|
+
it "skips to next non-info response" do
|
58
|
+
expect(subject.status_code).to eq(200)
|
59
|
+
expect(subject.headers).to eq("Content-Length" => "12")
|
60
|
+
expect(subject.read(12)).to eq("Hello World!")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "when response is feeded in many parts" do
|
65
|
+
let(:parts) { raw_response.chars }
|
66
|
+
|
67
|
+
it "skips to next non-info response" do
|
68
|
+
expect(subject.status_code).to eq(200)
|
69
|
+
expect(subject.headers).to eq("Content-Length" => "12")
|
70
|
+
expect(subject.read(12)).to eq("Hello World!")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
45
74
|
end
|
@@ -26,7 +26,7 @@ RSpec.describe HTTP::Response::Status do
|
|
26
26
|
end
|
27
27
|
|
28
28
|
described_class::REASONS.each do |code, reason|
|
29
|
-
class_eval <<-RUBY
|
29
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
30
30
|
context 'with well-known code: #{code}' do
|
31
31
|
let(:code) { #{code} }
|
32
32
|
it { is_expected.to eq #{reason.inspect} }
|
@@ -165,7 +165,7 @@ RSpec.describe HTTP::Response::Status do
|
|
165
165
|
end
|
166
166
|
|
167
167
|
described_class::SYMBOLS.each do |code, symbol|
|
168
|
-
class_eval <<-RUBY
|
168
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
169
169
|
context 'with well-known code: #{code}' do
|
170
170
|
let(:code) { #{code} }
|
171
171
|
it { is_expected.to be #{symbol.inspect} }
|
@@ -193,7 +193,7 @@ RSpec.describe HTTP::Response::Status do
|
|
193
193
|
end
|
194
194
|
|
195
195
|
described_class::SYMBOLS.each do |code, symbol|
|
196
|
-
class_eval <<-RUBY
|
196
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
197
197
|
describe '##{symbol}?' do
|
198
198
|
subject { status.#{symbol}? }
|
199
199
|
|
@@ -4,6 +4,7 @@ RSpec.describe HTTP::Response do
|
|
4
4
|
let(:body) { "Hello world!" }
|
5
5
|
let(:uri) { "http://example.com/" }
|
6
6
|
let(:headers) { {} }
|
7
|
+
let(:request) { HTTP::Request.new(:verb => :get, :uri => uri) }
|
7
8
|
|
8
9
|
subject(:response) do
|
9
10
|
HTTP::Response.new(
|
@@ -11,7 +12,7 @@ RSpec.describe HTTP::Response do
|
|
11
12
|
:version => "1.1",
|
12
13
|
:headers => headers,
|
13
14
|
:body => body,
|
14
|
-
:
|
15
|
+
:request => request
|
15
16
|
)
|
16
17
|
end
|
17
18
|
|
@@ -109,7 +110,7 @@ RSpec.describe HTTP::Response do
|
|
109
110
|
expect(response.parse("application/json")).to eq "foo" => "bar"
|
110
111
|
end
|
111
112
|
|
112
|
-
it "supports
|
113
|
+
it "supports mime type aliases" do
|
113
114
|
expect(response.parse(:json)).to eq "foo" => "bar"
|
114
115
|
end
|
115
116
|
end
|
@@ -165,7 +166,8 @@ RSpec.describe HTTP::Response do
|
|
165
166
|
HTTP::Response.new(
|
166
167
|
:version => "1.1",
|
167
168
|
:status => 200,
|
168
|
-
:connection => connection
|
169
|
+
:connection => connection,
|
170
|
+
:request => request
|
169
171
|
)
|
170
172
|
end
|
171
173
|
|
@@ -182,4 +184,79 @@ RSpec.describe HTTP::Response do
|
|
182
184
|
end
|
183
185
|
it { is_expected.not_to be_chunked }
|
184
186
|
end
|
187
|
+
|
188
|
+
describe "backwards compatibilty with :uri" do
|
189
|
+
context "with no :verb" do
|
190
|
+
subject(:response) do
|
191
|
+
HTTP::Response.new(
|
192
|
+
:status => 200,
|
193
|
+
:version => "1.1",
|
194
|
+
:headers => headers,
|
195
|
+
:body => body,
|
196
|
+
:uri => uri
|
197
|
+
)
|
198
|
+
end
|
199
|
+
|
200
|
+
it "defaults the uri to :uri" do
|
201
|
+
expect(response.request.uri.to_s).to eq uri
|
202
|
+
end
|
203
|
+
|
204
|
+
it "defaults to the verb to :get" do
|
205
|
+
expect(response.request.verb).to eq :get
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
context "with both a :request and :uri" do
|
210
|
+
subject(:response) do
|
211
|
+
HTTP::Response.new(
|
212
|
+
:status => 200,
|
213
|
+
:version => "1.1",
|
214
|
+
:headers => headers,
|
215
|
+
:body => body,
|
216
|
+
:uri => uri,
|
217
|
+
:request => request
|
218
|
+
)
|
219
|
+
end
|
220
|
+
|
221
|
+
it "raises ArgumentError" do
|
222
|
+
expect { response }.to raise_error(ArgumentError)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
describe "#body" do
|
228
|
+
let(:connection) { double(:sequence_id => 0) }
|
229
|
+
let(:chunks) { ["Hello, ", "World!"] }
|
230
|
+
|
231
|
+
subject(:response) do
|
232
|
+
HTTP::Response.new(
|
233
|
+
:status => 200,
|
234
|
+
:version => "1.1",
|
235
|
+
:headers => headers,
|
236
|
+
:request => request,
|
237
|
+
:connection => connection
|
238
|
+
)
|
239
|
+
end
|
240
|
+
|
241
|
+
before do
|
242
|
+
allow(connection).to receive(:readpartial) { chunks.shift }
|
243
|
+
allow(connection).to receive(:body_completed?) { chunks.empty? }
|
244
|
+
end
|
245
|
+
|
246
|
+
context "with no Content-Type" do
|
247
|
+
let(:headers) { {} }
|
248
|
+
|
249
|
+
it "returns a body with default binary encoding" do
|
250
|
+
expect(response.body.to_s.encoding).to eq Encoding::BINARY
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
context "with Content-Type: application/json" do
|
255
|
+
let(:headers) { {"Content-Type" => "application/json"} }
|
256
|
+
|
257
|
+
it "returns a body with a default UTF_8 encoding" do
|
258
|
+
expect(response.body.to_s.encoding).to eq Encoding::UTF_8
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
185
262
|
end
|
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
|
|
@@ -95,7 +95,8 @@ RSpec.describe HTTP do
|
|
95
95
|
expect(response.to_s).to match(/<!doctype html>/)
|
96
96
|
end
|
97
97
|
|
98
|
-
|
98
|
+
# TODO: htt:s://github.com/httprb/http/issues/627
|
99
|
+
xcontext "ssl" do
|
99
100
|
it "responds with the endpoint's body" do
|
100
101
|
response = ssl_client.via(proxy.addr, proxy.port).get dummy_ssl.endpoint
|
101
102
|
expect(response.to_s).to match(/<!doctype html>/)
|
@@ -131,7 +132,8 @@ RSpec.describe HTTP do
|
|
131
132
|
expect(response.status).to eq(407)
|
132
133
|
end
|
133
134
|
|
134
|
-
|
135
|
+
# TODO: htt:s://github.com/httprb/http/issues/627
|
136
|
+
xcontext "ssl" do
|
135
137
|
it "responds with the endpoint's body" do
|
136
138
|
response = ssl_client.via(proxy.addr, proxy.port, "username", "password").get dummy_ssl.endpoint
|
137
139
|
expect(response.to_s).to match(/<!doctype html>/)
|
@@ -307,6 +309,15 @@ RSpec.describe HTTP do
|
|
307
309
|
end
|
308
310
|
end
|
309
311
|
|
312
|
+
context "specifying per operation timeouts as frozen hash" do
|
313
|
+
let(:frozen_options) { {:read => 123}.freeze }
|
314
|
+
subject(:client) { HTTP.timeout(frozen_options) }
|
315
|
+
|
316
|
+
it "does not raise an error" do
|
317
|
+
expect { client }.not_to raise_error
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
310
321
|
context "specifying a global timeout" do
|
311
322
|
subject(:client) { HTTP.timeout 123 }
|
312
323
|
|
@@ -429,6 +440,22 @@ RSpec.describe HTTP do
|
|
429
440
|
|
430
441
|
expect(response.to_s).to eq("#{body}-deflated")
|
431
442
|
end
|
443
|
+
|
444
|
+
it "returns empty body for no content response where Content-Encoding is gzip" do
|
445
|
+
client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "gzip")
|
446
|
+
body = "Hello!"
|
447
|
+
response = client.post("#{dummy.endpoint}/no-content-204", :body => body)
|
448
|
+
|
449
|
+
expect(response.to_s).to eq("")
|
450
|
+
end
|
451
|
+
|
452
|
+
it "returns empty body for no content response where Content-Encoding is deflate" do
|
453
|
+
client = HTTP.use(:auto_inflate).headers("Accept-Encoding" => "deflate")
|
454
|
+
body = "Hello!"
|
455
|
+
response = client.post("#{dummy.endpoint}/no-content-204", :body => body)
|
456
|
+
|
457
|
+
expect(response.to_s).to eq("")
|
458
|
+
end
|
432
459
|
end
|
433
460
|
|
434
461
|
context "with :normalize_uri" do
|
data/spec/spec_helper.rb
CHANGED
@@ -1,19 +1,7 @@
|
|
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"
|
@@ -40,6 +28,13 @@ RSpec.configure do |config|
|
|
40
28
|
mocks.verify_partial_doubles = true
|
41
29
|
end
|
42
30
|
|
31
|
+
# This option will default to `:apply_to_host_groups` in RSpec 4 (and will
|
32
|
+
# have no way to turn it off -- the option exists only for backwards
|
33
|
+
# compatibility in RSpec 3). It causes shared context metadata to be
|
34
|
+
# inherited by the metadata hash of host groups and examples, rather than
|
35
|
+
# triggering implicit auto-inclusion in groups with matching metadata.
|
36
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
37
|
+
|
43
38
|
# These two settings work together to allow you to limit a spec run
|
44
39
|
# to individual examples or groups you care about by tagging them with
|
45
40
|
# `:focus` metadata. When nothing is tagged with `:focus`, all examples
|
@@ -48,17 +43,22 @@ RSpec.configure do |config|
|
|
48
43
|
config.filter_run_excluding :flaky if defined?(JRUBY_VERSION) && ENV["CI"]
|
49
44
|
config.run_all_when_everything_filtered = true
|
50
45
|
|
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
46
|
# This setting enables warnings. It's recommended, but in some cases may
|
59
47
|
# be too noisy due to issues in dependencies.
|
60
48
|
config.warnings = 0 == ENV["GUARD_RSPEC"].to_i
|
61
49
|
|
50
|
+
# Allows RSpec to persist some state between runs in order to support
|
51
|
+
# the `--only-failures` and `--next-failure` CLI options. We recommend
|
52
|
+
# you configure your source control system to ignore this file.
|
53
|
+
config.example_status_persistence_file_path = "spec/examples.txt"
|
54
|
+
|
55
|
+
# Limits the available syntax to the non-monkey patched syntax that is
|
56
|
+
# recommended. For more details, see:
|
57
|
+
# - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
|
58
|
+
# - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
59
|
+
# - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
|
60
|
+
config.disable_monkey_patching!
|
61
|
+
|
62
62
|
# Many RSpec users commonly either run the entire suite or an individual
|
63
63
|
# file, and it's useful to allow more verbose output when running an
|
64
64
|
# individual spec file.
|
data/spec/support/black_hole.rb
CHANGED
@@ -1,9 +1,8 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
1
|
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
3
|
|
4
4
|
class DummyServer < WEBrick::HTTPServer
|
5
|
-
# rubocop:disable Metrics/ClassLength
|
6
|
-
class Servlet < WEBrick::HTTPServlet::AbstractServlet
|
5
|
+
class Servlet < WEBrick::HTTPServlet::AbstractServlet # rubocop:disable Metrics/ClassLength
|
7
6
|
def self.sockets
|
8
7
|
@sockets ||= []
|
9
8
|
end
|
@@ -18,7 +17,7 @@ class DummyServer < WEBrick::HTTPServer
|
|
18
17
|
end
|
19
18
|
|
20
19
|
%w[get post head].each do |method|
|
21
|
-
class_eval <<-RUBY, __FILE__, __LINE__
|
20
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
22
21
|
def self.#{method}(path, &block)
|
23
22
|
handlers["#{method}:\#{path}"] = block
|
24
23
|
end
|
@@ -157,7 +156,7 @@ class DummyServer < WEBrick::HTTPServer
|
|
157
156
|
res.status = 200
|
158
157
|
|
159
158
|
res.body = case req["Accept-Encoding"]
|
160
|
-
when "gzip"
|
159
|
+
when "gzip"
|
161
160
|
res["Content-Encoding"] = "gzip"
|
162
161
|
StringIO.open do |out|
|
163
162
|
Zlib::GzipWriter.wrap(out) do |gz|
|
@@ -166,12 +165,24 @@ class DummyServer < WEBrick::HTTPServer
|
|
166
165
|
out.tap(&:rewind).read
|
167
166
|
end
|
168
167
|
end
|
169
|
-
when "deflate"
|
168
|
+
when "deflate"
|
170
169
|
res["Content-Encoding"] = "deflate"
|
171
170
|
Zlib::Deflate.deflate("#{req.body}-deflated")
|
172
171
|
else
|
173
172
|
"#{req.body}-raw"
|
174
173
|
end
|
175
174
|
end
|
175
|
+
|
176
|
+
post "/no-content-204" do |req, res|
|
177
|
+
res.status = 204
|
178
|
+
res.body = ""
|
179
|
+
|
180
|
+
case req["Accept-Encoding"]
|
181
|
+
when "gzip"
|
182
|
+
res["Content-Encoding"] = "gzip"
|
183
|
+
when "deflate"
|
184
|
+
res["Content-Encoding"] = "deflate"
|
185
|
+
end
|
186
|
+
end
|
176
187
|
end
|
177
188
|
end
|