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.
Files changed (93) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +67 -0
  3. data/.gitignore +6 -9
  4. data/.rspec +0 -4
  5. data/.rubocop/layout.yml +8 -0
  6. data/.rubocop/metrics.yml +4 -0
  7. data/.rubocop/rspec.yml +9 -0
  8. data/.rubocop/style.yml +32 -0
  9. data/.rubocop.yml +9 -108
  10. data/.rubocop_todo.yml +219 -0
  11. data/.yardopts +1 -1
  12. data/CHANGELOG.md +67 -0
  13. data/{CHANGES.md → CHANGES_OLD.md} +358 -0
  14. data/Gemfile +19 -10
  15. data/LICENSE.txt +1 -1
  16. data/README.md +53 -85
  17. data/Rakefile +3 -11
  18. data/SECURITY.md +17 -0
  19. data/http.gemspec +15 -6
  20. data/lib/http/base64.rb +12 -0
  21. data/lib/http/chainable.rb +71 -41
  22. data/lib/http/client.rb +73 -52
  23. data/lib/http/connection.rb +28 -18
  24. data/lib/http/content_type.rb +12 -7
  25. data/lib/http/errors.rb +19 -0
  26. data/lib/http/feature.rb +18 -1
  27. data/lib/http/features/auto_deflate.rb +27 -6
  28. data/lib/http/features/auto_inflate.rb +32 -6
  29. data/lib/http/features/instrumentation.rb +69 -0
  30. data/lib/http/features/logging.rb +53 -0
  31. data/lib/http/features/normalize_uri.rb +17 -0
  32. data/lib/http/features/raise_error.rb +22 -0
  33. data/lib/http/headers/known.rb +3 -0
  34. data/lib/http/headers/normalizer.rb +69 -0
  35. data/lib/http/headers.rb +72 -49
  36. data/lib/http/mime_type/adapter.rb +3 -1
  37. data/lib/http/mime_type/json.rb +1 -0
  38. data/lib/http/options.rb +31 -28
  39. data/lib/http/redirector.rb +56 -4
  40. data/lib/http/request/body.rb +31 -0
  41. data/lib/http/request/writer.rb +29 -9
  42. data/lib/http/request.rb +76 -41
  43. data/lib/http/response/body.rb +6 -4
  44. data/lib/http/response/inflater.rb +1 -1
  45. data/lib/http/response/parser.rb +78 -26
  46. data/lib/http/response/status.rb +4 -3
  47. data/lib/http/response.rb +45 -27
  48. data/lib/http/retriable/client.rb +37 -0
  49. data/lib/http/retriable/delay_calculator.rb +64 -0
  50. data/lib/http/retriable/errors.rb +14 -0
  51. data/lib/http/retriable/performer.rb +153 -0
  52. data/lib/http/timeout/global.rb +29 -47
  53. data/lib/http/timeout/null.rb +12 -8
  54. data/lib/http/timeout/per_operation.rb +32 -57
  55. data/lib/http/uri.rb +75 -1
  56. data/lib/http/version.rb +1 -1
  57. data/lib/http.rb +2 -2
  58. data/spec/lib/http/client_spec.rb +189 -36
  59. data/spec/lib/http/connection_spec.rb +31 -6
  60. data/spec/lib/http/features/auto_inflate_spec.rb +40 -23
  61. data/spec/lib/http/features/instrumentation_spec.rb +81 -0
  62. data/spec/lib/http/features/logging_spec.rb +65 -0
  63. data/spec/lib/http/features/raise_error_spec.rb +62 -0
  64. data/spec/lib/http/headers/normalizer_spec.rb +52 -0
  65. data/spec/lib/http/headers_spec.rb +53 -18
  66. data/spec/lib/http/options/headers_spec.rb +6 -2
  67. data/spec/lib/http/options/merge_spec.rb +16 -16
  68. data/spec/lib/http/redirector_spec.rb +147 -3
  69. data/spec/lib/http/request/body_spec.rb +71 -4
  70. data/spec/lib/http/request/writer_spec.rb +45 -2
  71. data/spec/lib/http/request_spec.rb +11 -5
  72. data/spec/lib/http/response/body_spec.rb +5 -5
  73. data/spec/lib/http/response/parser_spec.rb +74 -0
  74. data/spec/lib/http/response/status_spec.rb +3 -3
  75. data/spec/lib/http/response_spec.rb +83 -7
  76. data/spec/lib/http/retriable/delay_calculator_spec.rb +69 -0
  77. data/spec/lib/http/retriable/performer_spec.rb +302 -0
  78. data/spec/lib/http/uri/normalizer_spec.rb +95 -0
  79. data/spec/lib/http/uri_spec.rb +39 -0
  80. data/spec/lib/http_spec.rb +121 -68
  81. data/spec/regression_specs.rb +7 -0
  82. data/spec/spec_helper.rb +22 -21
  83. data/spec/support/black_hole.rb +1 -1
  84. data/spec/support/dummy_server/servlet.rb +42 -11
  85. data/spec/support/dummy_server.rb +9 -8
  86. data/spec/support/fuubar.rb +21 -0
  87. data/spec/support/http_handling_shared.rb +62 -66
  88. data/spec/support/simplecov.rb +19 -0
  89. data/spec/support/ssl_helper.rb +4 -4
  90. metadata +66 -27
  91. data/.coveralls.yml +0 -1
  92. data/.ruby-version +0 -1
  93. 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 "normalizes header name" do
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
- it "fails with invalid header name" do
39
- expect { headers.set "foo bar", "baz" }.
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 "normalizes header name" do
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 "normalizes header name" do
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
- it "fails with invalid header name" do
87
- expect { headers.delete "foo bar" }.
88
- to raise_error HTTP::HeaderError
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 "normalizes header name" do
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 :set_cookie, "hoo=ray"
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
- it "fails with invalid header name" do
121
- expect { headers.add "foo bar", "baz" }.
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
- it "fails with invalid header name" do
149
- expect { headers.get("foo bar") }.
150
- to raise_error HTTP::HeaderError
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", "set-cookie" => "woo=hoo"} }
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[Set-Cookie woo=hoo]
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("accept" => "json")
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 = Struct.new(:to_hash).new("accept" => "json")
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 => :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 => {}
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 => :parsed_body,
33
- :persistent => "https://www.googe.com",
34
- :params => {:plop => "plip"},
35
- :form => {:bar => "bar"},
36
- :body => "body-bar",
37
- :json => {:bar => "bar"},
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 => {:foo => "bar"},
42
- :proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080}
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(:body) { StringIO.new("a" * 16 * 1024 + "b" * 10 * 1024) }
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 "a" * 16 * 1024 + "b" * 10 * 1024
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 empty" do
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