http 4.4.0 → 5.0.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.yml +8 -110
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop_todo.yml +192 -0
- data/.yardopts +1 -1
- data/CHANGES.md +90 -0
- data/Gemfile +18 -10
- data/README.md +17 -20
- data/Rakefile +2 -10
- data/http.gemspec +3 -3
- data/lib/http/chainable.rb +23 -17
- data/lib/http/client.rb +36 -30
- data/lib/http/connection.rb +11 -7
- data/lib/http/content_type.rb +12 -7
- data/lib/http/feature.rb +3 -1
- data/lib/http/features/auto_deflate.rb +6 -6
- data/lib/http/features/auto_inflate.rb +6 -5
- 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 +2 -1
- data/lib/http/request.rb +13 -10
- data/lib/http/request/body.rb +1 -0
- data/lib/http/request/writer.rb +3 -2
- data/lib/http/response.rb +17 -15
- data/lib/http/response/body.rb +2 -2
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +75 -49
- data/lib/http/response/status.rb +4 -3
- data/lib/http/timeout/global.rb +17 -31
- data/lib/http/timeout/null.rb +2 -1
- data/lib/http/timeout/per_operation.rb +31 -54
- data/lib/http/uri.rb +5 -5
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +119 -30
- 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 +28 -21
- data/spec/lib/http/features/logging_spec.rb +8 -9
- 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 +2 -1
- 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 +74 -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 -3
- data/spec/spec_helper.rb +21 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server.rb +7 -7
- data/spec/support/dummy_server/servlet.rb +17 -6
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/http_handling_shared.rb +4 -4
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +21 -14
- data/.coveralls.yml +0 -1
- data/.travis.yml +0 -39
@@ -4,10 +4,8 @@ require "logger"
|
|
4
4
|
|
5
5
|
RSpec.describe HTTP::Features::Logging do
|
6
6
|
subject(:feature) do
|
7
|
-
logger
|
8
|
-
logger.formatter = ->(severity, _, _, message)
|
9
|
-
format("** %s **\n%s\n", severity, message)
|
10
|
-
end
|
7
|
+
logger = Logger.new(logdev)
|
8
|
+
logger.formatter = ->(severity, _, _, message) { format("** %s **\n%s\n", severity, message) }
|
11
9
|
|
12
10
|
described_class.new(:logger => logger)
|
13
11
|
end
|
@@ -17,10 +15,10 @@ RSpec.describe HTTP::Features::Logging do
|
|
17
15
|
describe "logging the request" do
|
18
16
|
let(:request) do
|
19
17
|
HTTP::Request.new(
|
20
|
-
:verb
|
21
|
-
:uri
|
22
|
-
:headers
|
23
|
-
:body
|
18
|
+
:verb => :post,
|
19
|
+
:uri => "https://example.com/",
|
20
|
+
:headers => {:accept => "application/json"},
|
21
|
+
:body => '{"hello": "world!"}'
|
24
22
|
)
|
25
23
|
end
|
26
24
|
|
@@ -47,7 +45,8 @@ RSpec.describe HTTP::Features::Logging do
|
|
47
45
|
:uri => "https://example.com",
|
48
46
|
:status => 200,
|
49
47
|
:headers => {:content_type => "application/json"},
|
50
|
-
:body => '{"success": true}'
|
48
|
+
:body => '{"success": true}',
|
49
|
+
:request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
|
51
50
|
)
|
52
51
|
end
|
53
52
|
|
@@ -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,8 +319,19 @@ 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(%w[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(%w[Set-Cookie Content-Type Set-Cookie X_kEy])
|
331
|
+
end
|
332
|
+
|
299
333
|
it "returns self instance if block given" do
|
300
|
-
expect(headers.each { |*| }).to be headers
|
334
|
+
expect(headers.each { |*| }).to be headers # rubocop:disable Lint/EmptyBlock
|
301
335
|
end
|
302
336
|
|
303
337
|
it "returns Enumerator if no block given" do
|
@@ -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(
|
@@ -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,74 @@
|
|
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.chars }
|
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
|
+
|
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
|
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
|
|
@@ -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
|
|