http 5.0.0.pre → 5.0.0.pre2
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/.rubocop.yml +17 -1
- data/.travis.yml +6 -4
- data/CHANGES.md +83 -0
- data/Gemfile +2 -1
- data/README.md +7 -6
- data/http.gemspec +11 -4
- data/lib/http/chainable.rb +8 -3
- data/lib/http/client.rb +32 -34
- data/lib/http/connection.rb +5 -5
- data/lib/http/content_type.rb +2 -2
- data/lib/http/feature.rb +3 -0
- data/lib/http/features/auto_deflate.rb +13 -7
- data/lib/http/features/auto_inflate.rb +6 -5
- data/lib/http/features/normalize_uri.rb +17 -0
- data/lib/http/headers.rb +48 -11
- data/lib/http/headers/known.rb +3 -0
- data/lib/http/mime_type/adapter.rb +1 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +4 -7
- data/lib/http/redirector.rb +3 -1
- data/lib/http/request.rb +32 -29
- data/lib/http/request/body.rb +26 -1
- data/lib/http/request/writer.rb +3 -2
- data/lib/http/response.rb +17 -15
- data/lib/http/response/body.rb +1 -0
- data/lib/http/response/parser.rb +20 -6
- data/lib/http/response/status.rb +2 -1
- data/lib/http/timeout/global.rb +1 -3
- data/lib/http/timeout/per_operation.rb +1 -0
- data/lib/http/uri.rb +13 -0
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +96 -14
- data/spec/lib/http/connection_spec.rb +8 -5
- data/spec/lib/http/features/auto_inflate_spec.rb +4 -2
- data/spec/lib/http/features/instrumentation_spec.rb +7 -6
- data/spec/lib/http/features/logging_spec.rb +6 -5
- data/spec/lib/http/headers_spec.rb +52 -17
- 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 +15 -1
- data/spec/lib/http/request/body_spec.rb +22 -0
- data/spec/lib/http/request/writer_spec.rb +13 -1
- data/spec/lib/http/request_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +45 -0
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +11 -22
- data/spec/lib/http_spec.rb +30 -1
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server.rb +6 -6
- data/spec/support/dummy_server/servlet.rb +8 -4
- data/spec/support/http_handling_shared.rb +4 -4
- data/spec/support/ssl_helper.rb +4 -4
- metadata +23 -16
@@ -17,10 +17,10 @@ RSpec.describe HTTP::Features::Logging do
|
|
17
17
|
describe "logging the request" do
|
18
18
|
let(:request) do
|
19
19
|
HTTP::Request.new(
|
20
|
-
:verb
|
21
|
-
:uri
|
22
|
-
:headers
|
23
|
-
:body
|
20
|
+
:verb => :post,
|
21
|
+
:uri => "https://example.com/",
|
22
|
+
:headers => {:accept => "application/json"},
|
23
|
+
:body => '{"hello": "world!"}'
|
24
24
|
)
|
25
25
|
end
|
26
26
|
|
@@ -47,7 +47,8 @@ RSpec.describe HTTP::Features::Logging do
|
|
47
47
|
:uri => "https://example.com",
|
48
48
|
:status => 200,
|
49
49
|
:headers => {:content_type => "application/json"},
|
50
|
-
:body => '{"success": true}'
|
50
|
+
:body => '{"success": true}',
|
51
|
+
:request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
|
51
52
|
)
|
52
53
|
end
|
53
54
|
|
@@ -13,7 +13,7 @@ RSpec.describe HTTP::Headers do
|
|
13
13
|
expect(headers["Accept"]).to eq "application/json"
|
14
14
|
end
|
15
15
|
|
16
|
-
it "
|
16
|
+
it "allows retrieval via normalized header name" do
|
17
17
|
headers.set :content_type, "application/json"
|
18
18
|
expect(headers["Content-Type"]).to eq "application/json"
|
19
19
|
end
|
@@ -35,8 +35,15 @@ RSpec.describe HTTP::Headers do
|
|
35
35
|
to raise_error HTTP::HeaderError
|
36
36
|
end
|
37
37
|
|
38
|
-
|
39
|
-
|
38
|
+
["foo bar", "foo bar: ok\nfoo", "evil-header: evil-value\nfoo"].each do |name|
|
39
|
+
it "fails with invalid header name (#{name.inspect})" do
|
40
|
+
expect { headers.set name, "baz" }.
|
41
|
+
to raise_error HTTP::HeaderError
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it "fails with invalid header value" do
|
46
|
+
expect { headers.set "foo", "bar\nEvil-Header: evil-value" }.
|
40
47
|
to raise_error HTTP::HeaderError
|
41
48
|
end
|
42
49
|
end
|
@@ -47,7 +54,7 @@ RSpec.describe HTTP::Headers do
|
|
47
54
|
expect(headers["Accept"]).to eq "application/json"
|
48
55
|
end
|
49
56
|
|
50
|
-
it "
|
57
|
+
it "allows retrieval via normalized header name" do
|
51
58
|
headers[:content_type] = "application/json"
|
52
59
|
expect(headers["Content-Type"]).to eq "application/json"
|
53
60
|
end
|
@@ -73,7 +80,7 @@ RSpec.describe HTTP::Headers do
|
|
73
80
|
expect(headers["Content-Type"]).to be_nil
|
74
81
|
end
|
75
82
|
|
76
|
-
it "
|
83
|
+
it "removes header that matches normalized version of specified name" do
|
77
84
|
headers.delete :content_type
|
78
85
|
expect(headers["Content-Type"]).to be_nil
|
79
86
|
end
|
@@ -83,9 +90,11 @@ RSpec.describe HTTP::Headers do
|
|
83
90
|
to raise_error HTTP::HeaderError
|
84
91
|
end
|
85
92
|
|
86
|
-
|
87
|
-
|
88
|
-
|
93
|
+
["foo bar", "foo bar: ok\nfoo"].each do |name|
|
94
|
+
it "fails with invalid header name (#{name.inspect})" do
|
95
|
+
expect { headers.delete name }.
|
96
|
+
to raise_error HTTP::HeaderError
|
97
|
+
end
|
89
98
|
end
|
90
99
|
end
|
91
100
|
|
@@ -95,13 +104,13 @@ RSpec.describe HTTP::Headers do
|
|
95
104
|
expect(headers["Accept"]).to eq "application/json"
|
96
105
|
end
|
97
106
|
|
98
|
-
it "
|
107
|
+
it "allows retrieval via normalized header name" do
|
99
108
|
headers.add :content_type, "application/json"
|
100
109
|
expect(headers["Content-Type"]).to eq "application/json"
|
101
110
|
end
|
102
111
|
|
103
112
|
it "appends new value if header exists" do
|
104
|
-
headers.add
|
113
|
+
headers.add "Set-Cookie", "hoo=ray"
|
105
114
|
headers.add :set_cookie, "woo=hoo"
|
106
115
|
expect(headers["Set-Cookie"]).to eq %w[hoo=ray woo=hoo]
|
107
116
|
end
|
@@ -117,8 +126,20 @@ RSpec.describe HTTP::Headers do
|
|
117
126
|
to raise_error HTTP::HeaderError
|
118
127
|
end
|
119
128
|
|
120
|
-
|
121
|
-
|
129
|
+
["foo bar", "foo bar: ok\nfoo"].each do |name|
|
130
|
+
it "fails with invalid header name (#{name.inspect})" do
|
131
|
+
expect { headers.add name, "baz" }.
|
132
|
+
to raise_error HTTP::HeaderError
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
it "fails with invalid header value" do
|
137
|
+
expect { headers.add "foo", "bar\nEvil-Header: evil-value" }.
|
138
|
+
to raise_error HTTP::HeaderError
|
139
|
+
end
|
140
|
+
|
141
|
+
it "fails when header name is not a String or Symbol" do
|
142
|
+
expect { headers.add 2, "foo" }.
|
122
143
|
to raise_error HTTP::HeaderError
|
123
144
|
end
|
124
145
|
end
|
@@ -145,9 +166,11 @@ RSpec.describe HTTP::Headers do
|
|
145
166
|
to raise_error HTTP::HeaderError
|
146
167
|
end
|
147
168
|
|
148
|
-
|
149
|
-
|
150
|
-
|
169
|
+
["foo bar", "foo bar: ok\nfoo"].each do |name|
|
170
|
+
it "fails with invalid header name (#{name.inspect})" do
|
171
|
+
expect { headers.get name }.
|
172
|
+
to raise_error HTTP::HeaderError
|
173
|
+
end
|
151
174
|
end
|
152
175
|
end
|
153
176
|
|
@@ -296,6 +319,17 @@ RSpec.describe HTTP::Headers do
|
|
296
319
|
)
|
297
320
|
end
|
298
321
|
|
322
|
+
it "yields header keys specified as symbols in normalized form" do
|
323
|
+
keys = headers.each.map(&:first)
|
324
|
+
expect(keys).to eq(["Set-Cookie", "Content-Type", "Set-Cookie"])
|
325
|
+
end
|
326
|
+
|
327
|
+
it "yields headers specified as strings without conversion" do
|
328
|
+
headers.add "X_kEy", "value"
|
329
|
+
keys = headers.each.map(&:first)
|
330
|
+
expect(keys).to eq(["Set-Cookie", "Content-Type", "Set-Cookie", "X_kEy"])
|
331
|
+
end
|
332
|
+
|
299
333
|
it "returns self instance if block given" do
|
300
334
|
expect(headers.each { |*| }).to be headers
|
301
335
|
end
|
@@ -472,14 +506,15 @@ RSpec.describe HTTP::Headers do
|
|
472
506
|
end
|
473
507
|
|
474
508
|
context "with duplicate header keys (mixed case)" do
|
475
|
-
let(:headers) { {"Set-Cookie" => "hoo=ray", "
|
509
|
+
let(:headers) { {"Set-Cookie" => "hoo=ray", "set_cookie" => "woo=hoo", :set_cookie => "ta=da"} }
|
476
510
|
|
477
511
|
it "adds all headers" do
|
478
512
|
expect(described_class.coerce(headers).to_a).
|
479
513
|
to match_array(
|
480
514
|
[
|
481
515
|
%w[Set-Cookie hoo=ray],
|
482
|
-
%w[
|
516
|
+
%w[set_cookie woo=hoo],
|
517
|
+
%w[Set-Cookie ta=da]
|
483
518
|
]
|
484
519
|
)
|
485
520
|
end
|
@@ -8,7 +8,7 @@ RSpec.describe HTTP::Options, "headers" do
|
|
8
8
|
end
|
9
9
|
|
10
10
|
it "may be specified with with_headers" do
|
11
|
-
opts2 = opts.with_headers(
|
11
|
+
opts2 = opts.with_headers(:accept => "json")
|
12
12
|
expect(opts.headers).to be_empty
|
13
13
|
expect(opts2.headers).to eq([%w[Accept json]])
|
14
14
|
end
|
@@ -18,28 +18,28 @@ RSpec.describe HTTP::Options, "merge" do
|
|
18
18
|
# FIXME: yuck :(
|
19
19
|
|
20
20
|
foo = HTTP::Options.new(
|
21
|
-
:response
|
22
|
-
:params
|
23
|
-
:form
|
24
|
-
:body
|
25
|
-
:json
|
26
|
-
:headers
|
27
|
-
:proxy
|
28
|
-
:features
|
21
|
+
:response => :body,
|
22
|
+
:params => {:baz => "bar"},
|
23
|
+
:form => {:foo => "foo"},
|
24
|
+
:body => "body-foo",
|
25
|
+
:json => {:foo => "foo"},
|
26
|
+
:headers => {:accept => "json", :foo => "foo"},
|
27
|
+
:proxy => {},
|
28
|
+
:features => {}
|
29
29
|
)
|
30
30
|
|
31
31
|
bar = HTTP::Options.new(
|
32
|
-
:response
|
33
|
-
:persistent
|
34
|
-
:params
|
35
|
-
:form
|
36
|
-
:body
|
37
|
-
:json
|
32
|
+
:response => :parsed_body,
|
33
|
+
:persistent => "https://www.googe.com",
|
34
|
+
:params => {:plop => "plip"},
|
35
|
+
:form => {:bar => "bar"},
|
36
|
+
:body => "body-bar",
|
37
|
+
:json => {:bar => "bar"},
|
38
38
|
:keep_alive_timeout => 10,
|
39
39
|
:headers => {:accept => "xml", :bar => "bar"},
|
40
40
|
:timeout_options => {:foo => :bar},
|
41
|
-
:ssl
|
42
|
-
:proxy
|
41
|
+
:ssl => {:foo => "bar"},
|
42
|
+
:proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080}
|
43
43
|
)
|
44
44
|
|
45
45
|
expect(foo.merge(bar).to_hash).to eq(
|
@@ -6,7 +6,8 @@ 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
|
|
@@ -75,6 +76,19 @@ RSpec.describe HTTP::Redirector do
|
|
75
76
|
expect(res.to_s).to eq "foo"
|
76
77
|
end
|
77
78
|
|
79
|
+
it "concatenates multiple Location headers" do
|
80
|
+
req = HTTP::Request.new :verb => :head, :uri => "http://example.com"
|
81
|
+
headers = HTTP::Headers.new
|
82
|
+
|
83
|
+
%w[http://example.com /123].each { |loc| headers.add("Location", loc) }
|
84
|
+
|
85
|
+
res = redirector.perform(req, simple_response(301, "", headers)) do |redirect|
|
86
|
+
simple_response(200, redirect.uri.to_s)
|
87
|
+
end
|
88
|
+
|
89
|
+
expect(res.to_s).to eq "http://example.com/123"
|
90
|
+
end
|
91
|
+
|
78
92
|
context "following 300 redirect" do
|
79
93
|
context "with strict mode" do
|
80
94
|
let(:options) { {:strict => true} }
|
@@ -125,6 +125,28 @@ RSpec.describe HTTP::Request::Body do
|
|
125
125
|
end
|
126
126
|
end
|
127
127
|
|
128
|
+
context "when body is a pipe" do
|
129
|
+
let(:ios) { IO.pipe }
|
130
|
+
let(:body) { ios[0] }
|
131
|
+
|
132
|
+
around do |example|
|
133
|
+
writer = Thread.new(ios[1]) do |io|
|
134
|
+
io << "abcdef"
|
135
|
+
io.close
|
136
|
+
end
|
137
|
+
|
138
|
+
begin
|
139
|
+
example.run
|
140
|
+
ensure
|
141
|
+
writer.join
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
it "yields chunks of content" do
|
146
|
+
expect(chunks.inject("", :+)).to eq("abcdef")
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
128
150
|
context "when body is an Enumerable IO" do
|
129
151
|
let(:data) { "a" * 16 * 1024 + "b" * 10 * 1024 }
|
130
152
|
let(:body) { StringIO.new data }
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe HTTP::Response::Parser do
|
4
|
+
subject(:parser) { described_class.new }
|
5
|
+
let(:raw_response) do
|
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
|
+
end
|
8
|
+
let(:expected_headers) do
|
9
|
+
{
|
10
|
+
"Content-Length" => "2",
|
11
|
+
"Content-Type" => "application/json",
|
12
|
+
"MyHeader" => "val",
|
13
|
+
"EmptyHeader" => ""
|
14
|
+
}
|
15
|
+
end
|
16
|
+
let(:expected_body) { "{}" }
|
17
|
+
|
18
|
+
before do
|
19
|
+
parts.each { |part| subject.add(part) }
|
20
|
+
end
|
21
|
+
|
22
|
+
context "whole response in one part" do
|
23
|
+
let(:parts) { [raw_response] }
|
24
|
+
|
25
|
+
it "parses headers" do
|
26
|
+
expect(subject.headers.to_h).to eq(expected_headers)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "parses body" do
|
30
|
+
expect(subject.read(expected_body.size)).to eq(expected_body)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "response in many parts" do
|
35
|
+
let(:parts) { raw_response.split(//) }
|
36
|
+
|
37
|
+
it "parses headers" do
|
38
|
+
expect(subject.headers.to_h).to eq(expected_headers)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "parses body" do
|
42
|
+
expect(subject.read(expected_body.size)).to eq(expected_body)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
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
|
|
@@ -11,7 +11,8 @@ RSpec.describe HTTP::Response do
|
|
11
11
|
:version => "1.1",
|
12
12
|
:headers => headers,
|
13
13
|
:body => body,
|
14
|
-
:uri => uri
|
14
|
+
:uri => uri,
|
15
|
+
:request => HTTP::Request.new(:verb => :get, :uri => "http://example.com")
|
15
16
|
)
|
16
17
|
end
|
17
18
|
|
@@ -86,32 +87,19 @@ RSpec.describe HTTP::Response do
|
|
86
87
|
end
|
87
88
|
|
88
89
|
describe "#parse" do
|
89
|
-
let(:headers) { {"Content-Type" =>
|
90
|
+
let(:headers) { {"Content-Type" => "application/json"} }
|
90
91
|
let(:body) { '{"foo":"bar"}' }
|
91
92
|
|
92
|
-
|
93
|
-
|
94
|
-
it "returns parsed body" do
|
95
|
-
expect(response.parse).to eq "foo" => "bar"
|
96
|
-
end
|
93
|
+
it "fails if MIME type decoder is not found" do
|
94
|
+
expect { response.parse "text/html" }.to raise_error(HTTP::Error)
|
97
95
|
end
|
98
96
|
|
99
|
-
|
100
|
-
|
101
|
-
it "raises HTTP::Error" do
|
102
|
-
expect { response.parse }.to raise_error HTTP::Error
|
103
|
-
end
|
97
|
+
it "uses decoder found by given MIME type" do
|
98
|
+
expect(response.parse("application/json")).to eq("foo" => "bar")
|
104
99
|
end
|
105
100
|
|
106
|
-
|
107
|
-
|
108
|
-
it "ignores mime_type of response" do
|
109
|
-
expect(response.parse("application/json")).to eq "foo" => "bar"
|
110
|
-
end
|
111
|
-
|
112
|
-
it "supports MIME type aliases" do
|
113
|
-
expect(response.parse(:json)).to eq "foo" => "bar"
|
114
|
-
end
|
101
|
+
it "uses decoder found by given MIME type alias" do
|
102
|
+
expect(response.parse(:json)).to eq("foo" => "bar")
|
115
103
|
end
|
116
104
|
end
|
117
105
|
|
@@ -165,7 +153,8 @@ RSpec.describe HTTP::Response do
|
|
165
153
|
HTTP::Response.new(
|
166
154
|
:version => "1.1",
|
167
155
|
:status => 200,
|
168
|
-
:connection => connection
|
156
|
+
:connection => connection,
|
157
|
+
:request => HTTP::Request.new(:verb => :get, :uri => "http://example.com")
|
169
158
|
)
|
170
159
|
end
|
171
160
|
|