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,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe HTTP::Features::RaiseError do
|
|
4
|
+
subject(:feature) { described_class.new(ignore: ignore) }
|
|
5
|
+
|
|
6
|
+
let(:connection) { double }
|
|
7
|
+
let(:status) { 200 }
|
|
8
|
+
let(:ignore) { [] }
|
|
9
|
+
|
|
10
|
+
describe "#wrap_response" do
|
|
11
|
+
subject(:result) { feature.wrap_response(response) }
|
|
12
|
+
|
|
13
|
+
let(:response) do
|
|
14
|
+
HTTP::Response.new(
|
|
15
|
+
version: "1.1",
|
|
16
|
+
status: status,
|
|
17
|
+
headers: {},
|
|
18
|
+
connection: connection,
|
|
19
|
+
request: HTTP::Request.new(verb: :get, uri: "https://example.com")
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
context "when status is 200" do
|
|
24
|
+
it "returns original request" do
|
|
25
|
+
expect(result).to be response
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
context "when status is 399" do
|
|
30
|
+
let(:status) { 399 }
|
|
31
|
+
|
|
32
|
+
it "returns original request" do
|
|
33
|
+
expect(result).to be response
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context "when status is 400" do
|
|
38
|
+
let(:status) { 400 }
|
|
39
|
+
|
|
40
|
+
it "raises" do
|
|
41
|
+
expect { result }.to raise_error(HTTP::StatusError, "Unexpected status code 400")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
context "when status is 599" do
|
|
46
|
+
let(:status) { 599 }
|
|
47
|
+
|
|
48
|
+
it "raises" do
|
|
49
|
+
expect { result }.to raise_error(HTTP::StatusError, "Unexpected status code 599")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
context "when error status is ignored" do
|
|
54
|
+
let(:status) { 500 }
|
|
55
|
+
let(:ignore) { [500] }
|
|
56
|
+
|
|
57
|
+
it "returns original request" do
|
|
58
|
+
expect(result).to be response
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe HTTP::Headers::Normalizer do
|
|
4
|
+
subject(:normalizer) { described_class.new }
|
|
5
|
+
|
|
6
|
+
include_context RSpec::Memory
|
|
7
|
+
|
|
8
|
+
describe "#call" do
|
|
9
|
+
it "normalizes the header" do
|
|
10
|
+
expect(normalizer.call("content_type")).to eq "Content-Type"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it "returns a non-frozen string" do
|
|
14
|
+
expect(normalizer.call("content_type")).not_to be_frozen
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "evicts the oldest item when cache is full" do
|
|
18
|
+
max_headers = (1..described_class::Cache::MAX_SIZE).map { |i| "Header#{i}" }
|
|
19
|
+
max_headers.each { |header| normalizer.call(header) }
|
|
20
|
+
normalizer.call("New-Header")
|
|
21
|
+
cache_store = normalizer.instance_variable_get(:@cache).instance_variable_get(:@store)
|
|
22
|
+
expect(cache_store.keys).to eq(max_headers[1..] + ["New-Header"])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "retuns mutable strings" do
|
|
26
|
+
normalized_headers = Array.new(3) { normalizer.call("content_type") }
|
|
27
|
+
|
|
28
|
+
expect(normalized_headers)
|
|
29
|
+
.to satisfy { |arr| arr.uniq.size == 1 }
|
|
30
|
+
.and(satisfy { |arr| arr.map(&:object_id).uniq.size == normalized_headers.size })
|
|
31
|
+
.and(satisfy { |arr| arr.none?(&:frozen?) })
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "allocates minimal memory for normalization of the same header" do
|
|
35
|
+
normalizer.call("accept") # XXX: Ensure normalizer is pre-allocated
|
|
36
|
+
|
|
37
|
+
# On first call it is expected to allocate during normalization
|
|
38
|
+
expect { normalizer.call("content_type") }.to limit_allocations(
|
|
39
|
+
Array => 1,
|
|
40
|
+
MatchData => 1,
|
|
41
|
+
String => 6
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# On subsequent call it is expected to only allocate copy of a cached string
|
|
45
|
+
expect { normalizer.call("content_type") }.to limit_allocations(
|
|
46
|
+
Array => 0,
|
|
47
|
+
MatchData => 0,
|
|
48
|
+
String => 1
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -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,13 +8,17 @@ 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
|
|
15
15
|
|
|
16
16
|
it "accepts any object that respond to :to_hash" do
|
|
17
|
-
x =
|
|
17
|
+
x = if RUBY_VERSION >= "3.2.0"
|
|
18
|
+
Data.define(:to_hash).new(:to_hash => { "accept" => "json" })
|
|
19
|
+
else
|
|
20
|
+
Struct.new(:to_hash).new({ "accept" => "json" })
|
|
21
|
+
end
|
|
18
22
|
expect(opts.with_headers(x).headers["accept"]).to eq("json")
|
|
19
23
|
end
|
|
20
24
|
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,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
|
|
@@ -75,6 +80,101 @@ RSpec.describe HTTP::Redirector do
|
|
|
75
80
|
expect(res.to_s).to eq "foo"
|
|
76
81
|
end
|
|
77
82
|
|
|
83
|
+
it "concatenates multiple Location headers" do
|
|
84
|
+
req = HTTP::Request.new :verb => :head, :uri => "http://example.com"
|
|
85
|
+
headers = HTTP::Headers.new
|
|
86
|
+
|
|
87
|
+
%w[http://example.com /123].each { |loc| headers.add("Location", loc) }
|
|
88
|
+
|
|
89
|
+
res = redirector.perform(req, simple_response(301, "", headers)) do |redirect|
|
|
90
|
+
simple_response(200, redirect.uri.to_s)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
expect(res.to_s).to eq "http://example.com/123"
|
|
94
|
+
end
|
|
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
|
+
|
|
119
|
+
expect(res.to_s).to eq "bar"
|
|
120
|
+
expect(res.cookies.cookies.to_h { |c| [c.name, c.value] }).to eq({
|
|
121
|
+
"foo" => "42",
|
|
122
|
+
"bar" => "53",
|
|
123
|
+
"baz" => "65"
|
|
124
|
+
})
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "returns original cookies in response" do
|
|
128
|
+
req = HTTP::Request.new :verb => :head, :uri => "http://example.com"
|
|
129
|
+
req.headers.set("Cookie", "foo=42; deleted=baz")
|
|
130
|
+
hops = [
|
|
131
|
+
redirect_response(301, "http://example.com/1", {"bar" => "64", "deleted" => ""}),
|
|
132
|
+
simple_response(200, "bar")
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
request_cookies = [
|
|
136
|
+
{"foo" => "42", "bar" => "64"},
|
|
137
|
+
{"foo" => "42", "bar" => "64"}
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
res = redirector.perform(req, hops.shift) do |request|
|
|
141
|
+
req_cookie = HTTP::Cookie.cookie_value_to_hash(request.headers["Cookie"] || "")
|
|
142
|
+
expect(req_cookie).to eq request_cookies.shift
|
|
143
|
+
hops.shift
|
|
144
|
+
end
|
|
145
|
+
expect(res.to_s).to eq "bar"
|
|
146
|
+
cookies = res.cookies.cookies.to_h { |c| [c.name, c.value] }
|
|
147
|
+
expect(cookies["foo"]).to eq "42"
|
|
148
|
+
expect(cookies["bar"]).to eq "64"
|
|
149
|
+
expect(cookies["deleted"]).to eq nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
context "with on_redirect callback" do
|
|
153
|
+
let(:options) do
|
|
154
|
+
{
|
|
155
|
+
:on_redirect => proc do |response, location|
|
|
156
|
+
@redirect_response = response
|
|
157
|
+
@redirect_location = location
|
|
158
|
+
end
|
|
159
|
+
}
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "calls on_redirect" do
|
|
163
|
+
req = HTTP::Request.new :verb => :head, :uri => "http://example.com"
|
|
164
|
+
hops = [
|
|
165
|
+
redirect_response(301, "http://example.com/1"),
|
|
166
|
+
redirect_response(301, "http://example.com/2"),
|
|
167
|
+
simple_response(200, "foo")
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
redirector.perform(req, hops.shift) do |prev_req, _|
|
|
171
|
+
expect(@redirect_location.uri.to_s).to eq prev_req.uri.to_s
|
|
172
|
+
expect(@redirect_response.code).to eq 301
|
|
173
|
+
hops.shift
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
78
178
|
context "following 300 redirect" do
|
|
79
179
|
context "with strict mode" do
|
|
80
180
|
let(:options) { {:strict => true} }
|
|
@@ -382,5 +482,49 @@ RSpec.describe HTTP::Redirector do
|
|
|
382
482
|
end
|
|
383
483
|
end
|
|
384
484
|
end
|
|
485
|
+
|
|
486
|
+
describe "changing verbs during redirects" do
|
|
487
|
+
let(:options) { {:strict => false} }
|
|
488
|
+
let(:post_body) { HTTP::Request::Body.new("i might be way longer in real life") }
|
|
489
|
+
let(:cookie) { "dont=eat my cookies" }
|
|
490
|
+
|
|
491
|
+
def a_dangerous_request(verb)
|
|
492
|
+
HTTP::Request.new(
|
|
493
|
+
:verb => verb, :uri => "http://example.com",
|
|
494
|
+
:body => post_body, :headers => {
|
|
495
|
+
"Content-Type" => "meme",
|
|
496
|
+
"Cookie" => cookie
|
|
497
|
+
}
|
|
498
|
+
)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def empty_body
|
|
502
|
+
HTTP::Request::Body.new(nil)
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
it "follows without body/content type if it has to change verb" do
|
|
506
|
+
req = a_dangerous_request(:post)
|
|
507
|
+
res = redirect_response 302, "http://example.com/1"
|
|
508
|
+
|
|
509
|
+
redirector.perform(req, res) do |prev_req, _|
|
|
510
|
+
expect(prev_req.body).to eq(empty_body)
|
|
511
|
+
expect(prev_req.headers["Cookie"]).to eq(cookie)
|
|
512
|
+
expect(prev_req.headers["Content-Type"]).to eq(nil)
|
|
513
|
+
simple_response 200
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
it "leaves body/content-type intact if it does not have to change verb" do
|
|
518
|
+
req = a_dangerous_request(:post)
|
|
519
|
+
res = redirect_response 307, "http://example.com/1"
|
|
520
|
+
|
|
521
|
+
redirector.perform(req, res) do |prev_req, _|
|
|
522
|
+
expect(prev_req.body).to eq(post_body)
|
|
523
|
+
expect(prev_req.headers["Cookie"]).to eq(cookie)
|
|
524
|
+
expect(prev_req.headers["Content-Type"]).to eq("meme")
|
|
525
|
+
simple_response 200
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end
|
|
385
529
|
end
|
|
386
530
|
end
|
|
@@ -118,18 +118,56 @@ 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
|
+
end
|
|
126
|
+
end
|
|
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")
|
|
125
147
|
end
|
|
126
148
|
end
|
|
127
149
|
|
|
128
150
|
context "when body is an Enumerable IO" do
|
|
129
|
-
let(:
|
|
151
|
+
let(:data) { ("a" * 16 * 1024) + ("b" * 10 * 1024) }
|
|
152
|
+
let(:body) { StringIO.new data }
|
|
130
153
|
|
|
131
154
|
it "yields chunks of content" do
|
|
132
|
-
expect(chunks.inject("", :+)).to eq
|
|
155
|
+
expect(chunks.inject("", :+)).to eq data
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "allows to enumerate multiple times" do
|
|
159
|
+
results = []
|
|
160
|
+
|
|
161
|
+
2.times do
|
|
162
|
+
result = ""
|
|
163
|
+
subject.each { |chunk| result += chunk }
|
|
164
|
+
results << result
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
aggregate_failures do
|
|
168
|
+
expect(results.count).to eq 2
|
|
169
|
+
expect(results).to all eq data
|
|
170
|
+
end
|
|
133
171
|
end
|
|
134
172
|
end
|
|
135
173
|
|
|
@@ -141,4 +179,33 @@ RSpec.describe HTTP::Request::Body do
|
|
|
141
179
|
end
|
|
142
180
|
end
|
|
143
181
|
end
|
|
182
|
+
|
|
183
|
+
describe "#==" do
|
|
184
|
+
context "when sources are equivalent" do
|
|
185
|
+
let(:body1) { HTTP::Request::Body.new("content") }
|
|
186
|
+
let(:body2) { HTTP::Request::Body.new("content") }
|
|
187
|
+
|
|
188
|
+
it "returns true" do
|
|
189
|
+
expect(body1).to eq body2
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
context "when sources are not equivalent" do
|
|
194
|
+
let(:body1) { HTTP::Request::Body.new("content") }
|
|
195
|
+
let(:body2) { HTTP::Request::Body.new(nil) }
|
|
196
|
+
|
|
197
|
+
it "returns false" do
|
|
198
|
+
expect(body1).not_to eq body2
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
context "when objects are not of the same class" do
|
|
203
|
+
let(:body1) { HTTP::Request::Body.new("content") }
|
|
204
|
+
let(:body2) { "content" }
|
|
205
|
+
|
|
206
|
+
it "returns false" do
|
|
207
|
+
expect(body1).not_to eq body2
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
144
211
|
end
|
|
@@ -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 [
|
|
@@ -74,5 +97,25 @@ RSpec.describe HTTP::Request::Writer do
|
|
|
74
97
|
].join
|
|
75
98
|
end
|
|
76
99
|
end
|
|
100
|
+
|
|
101
|
+
context "when server won't accept any more data" do
|
|
102
|
+
before do
|
|
103
|
+
expect(io).to receive(:write).and_raise(Errno::EPIPE)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "aborts silently" do
|
|
107
|
+
writer.stream
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
context "when writing to socket raises an exception" do
|
|
112
|
+
before do
|
|
113
|
+
expect(io).to receive(:write).and_raise(Errno::ECONNRESET)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "raises a ConnectionError" do
|
|
117
|
+
expect { writer.stream }.to raise_error(HTTP::ConnectionError)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
77
120
|
end
|
|
78
121
|
end
|