http 5.0.0.pre2 → 5.0.2

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +65 -0
  3. data/.gitignore +6 -10
  4. data/.rspec +0 -4
  5. data/.rubocop/layout.yml +8 -0
  6. data/.rubocop/style.yml +32 -0
  7. data/.rubocop.yml +7 -124
  8. data/.rubocop_todo.yml +192 -0
  9. data/CHANGES.md +114 -1
  10. data/Gemfile +18 -11
  11. data/LICENSE.txt +1 -1
  12. data/README.md +13 -16
  13. data/Rakefile +2 -10
  14. data/http.gemspec +3 -3
  15. data/lib/http/chainable.rb +15 -14
  16. data/lib/http/client.rb +26 -15
  17. data/lib/http/connection.rb +7 -3
  18. data/lib/http/content_type.rb +10 -5
  19. data/lib/http/feature.rb +1 -1
  20. data/lib/http/features/auto_inflate.rb +0 -2
  21. data/lib/http/features/instrumentation.rb +1 -1
  22. data/lib/http/features/logging.rb +19 -21
  23. data/lib/http/headers.rb +3 -3
  24. data/lib/http/mime_type/adapter.rb +2 -0
  25. data/lib/http/options.rb +2 -2
  26. data/lib/http/redirector.rb +1 -1
  27. data/lib/http/request/writer.rb +5 -1
  28. data/lib/http/request.rb +22 -5
  29. data/lib/http/response/body.rb +5 -4
  30. data/lib/http/response/inflater.rb +1 -1
  31. data/lib/http/response/parser.rb +74 -62
  32. data/lib/http/response/status.rb +2 -2
  33. data/lib/http/response.rb +22 -4
  34. data/lib/http/timeout/global.rb +41 -35
  35. data/lib/http/timeout/null.rb +2 -1
  36. data/lib/http/timeout/per_operation.rb +56 -59
  37. data/lib/http/version.rb +1 -1
  38. data/spec/lib/http/client_spec.rb +109 -41
  39. data/spec/lib/http/features/auto_inflate_spec.rb +0 -1
  40. data/spec/lib/http/features/instrumentation_spec.rb +21 -16
  41. data/spec/lib/http/features/logging_spec.rb +2 -5
  42. data/spec/lib/http/headers_spec.rb +3 -3
  43. data/spec/lib/http/redirector_spec.rb +44 -0
  44. data/spec/lib/http/request/writer_spec.rb +12 -1
  45. data/spec/lib/http/response/body_spec.rb +5 -5
  46. data/spec/lib/http/response/parser_spec.rb +30 -1
  47. data/spec/lib/http/response_spec.rb +62 -10
  48. data/spec/lib/http_spec.rb +20 -2
  49. data/spec/spec_helper.rb +21 -21
  50. data/spec/support/black_hole.rb +1 -1
  51. data/spec/support/dummy_server/servlet.rb +14 -2
  52. data/spec/support/dummy_server.rb +1 -1
  53. data/spec/support/fuubar.rb +21 -0
  54. data/spec/support/simplecov.rb +19 -0
  55. metadata +23 -17
  56. data/.coveralls.yml +0 -1
  57. data/.travis.yml +0 -38
@@ -74,7 +74,6 @@ RSpec.describe HTTP::Features::AutoInflate do
74
74
  :status => 200,
75
75
  :headers => {:content_encoding => "gzip"},
76
76
  :connection => connection,
77
- :uri => "https://example.com",
78
77
  :request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
79
78
  )
80
79
  end
@@ -2,8 +2,29 @@
2
2
 
3
3
  RSpec.describe HTTP::Features::Instrumentation do
4
4
  subject(:feature) { HTTP::Features::Instrumentation.new(:instrumenter => instrumenter) }
5
+
5
6
  let(:instrumenter) { TestInstrumenter.new }
6
7
 
8
+ before do
9
+ test_instrumenter = Class.new(HTTP::Features::Instrumentation::NullInstrumenter) do
10
+ attr_reader :output
11
+
12
+ def initialize
13
+ @output = {}
14
+ end
15
+
16
+ def start(_name, payload)
17
+ output[:start] = payload
18
+ end
19
+
20
+ def finish(_name, payload)
21
+ output[:finish] = payload
22
+ end
23
+ end
24
+
25
+ stub_const("TestInstrumenter", test_instrumenter)
26
+ end
27
+
7
28
  describe "logging the request" do
8
29
  let(:request) do
9
30
  HTTP::Request.new(
@@ -25,7 +46,6 @@ RSpec.describe HTTP::Features::Instrumentation do
25
46
  let(:response) do
26
47
  HTTP::Response.new(
27
48
  :version => "1.1",
28
- :uri => "https://example.com",
29
49
  :status => 200,
30
50
  :headers => {:content_type => "application/json"},
31
51
  :body => '{"success": true}',
@@ -39,19 +59,4 @@ RSpec.describe HTTP::Features::Instrumentation do
39
59
  expect(instrumenter.output[:finish]).to eq(:response => response)
40
60
  end
41
61
  end
42
-
43
- class TestInstrumenter < HTTP::Features::Instrumentation::NullInstrumenter
44
- attr_reader :output
45
- def initialize
46
- @output = {}
47
- end
48
-
49
- def start(_name, payload)
50
- output[:start] = payload
51
- end
52
-
53
- def finish(_name, payload)
54
- output[:finish] = payload
55
- end
56
- end
57
62
  end
@@ -4,10 +4,8 @@ require "logger"
4
4
 
5
5
  RSpec.describe HTTP::Features::Logging do
6
6
  subject(:feature) do
7
- logger = Logger.new(logdev)
8
- logger.formatter = ->(severity, _, _, message) do
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
@@ -44,7 +42,6 @@ RSpec.describe HTTP::Features::Logging do
44
42
  let(:response) do
45
43
  HTTP::Response.new(
46
44
  :version => "1.1",
47
- :uri => "https://example.com",
48
45
  :status => 200,
49
46
  :headers => {:content_type => "application/json"},
50
47
  :body => '{"success": true}',
@@ -321,17 +321,17 @@ RSpec.describe HTTP::Headers do
321
321
 
322
322
  it "yields header keys specified as symbols in normalized form" do
323
323
  keys = headers.each.map(&:first)
324
- expect(keys).to eq(["Set-Cookie", "Content-Type", "Set-Cookie"])
324
+ expect(keys).to eq(%w[Set-Cookie Content-Type Set-Cookie])
325
325
  end
326
326
 
327
327
  it "yields headers specified as strings without conversion" do
328
328
  headers.add "X_kEy", "value"
329
329
  keys = headers.each.map(&:first)
330
- expect(keys).to eq(["Set-Cookie", "Content-Type", "Set-Cookie", "X_kEy"])
330
+ expect(keys).to eq(%w[Set-Cookie Content-Type Set-Cookie X_kEy])
331
331
  end
332
332
 
333
333
  it "returns self instance if block given" do
334
- expect(headers.each { |*| }).to be headers
334
+ expect(headers.each { |*| }).to be headers # rubocop:disable Lint/EmptyBlock
335
335
  end
336
336
 
337
337
  it "returns Enumerator if no block given" do
@@ -396,5 +396,49 @@ RSpec.describe HTTP::Redirector do
396
396
  end
397
397
  end
398
398
  end
399
+
400
+ describe "changing verbs during redirects" do
401
+ let(:options) { {:strict => false} }
402
+ let(:post_body) { HTTP::Request::Body.new("i might be way longer in real life") }
403
+ let(:cookie) { "dont eat my cookies" }
404
+
405
+ def a_dangerous_request(verb)
406
+ HTTP::Request.new(
407
+ :verb => verb, :uri => "http://example.com",
408
+ :body => post_body, :headers => {
409
+ "Content-Type" => "meme",
410
+ "Cookie" => cookie
411
+ }
412
+ )
413
+ end
414
+
415
+ def empty_body
416
+ HTTP::Request::Body.new(nil)
417
+ end
418
+
419
+ it "follows without body/content type if it has to change verb" do
420
+ req = a_dangerous_request(:post)
421
+ res = redirect_response 302, "http://example.com/1"
422
+
423
+ redirector.perform(req, res) do |prev_req, _|
424
+ expect(prev_req.body).to eq(empty_body)
425
+ expect(prev_req.headers["Cookie"]).to eq(cookie)
426
+ expect(prev_req.headers["Content-Type"]).to eq(nil)
427
+ simple_response 200
428
+ end
429
+ end
430
+
431
+ it "leaves body/content-type intact if it does not have to change verb" do
432
+ req = a_dangerous_request(:post)
433
+ res = redirect_response 307, "http://example.com/1"
434
+
435
+ redirector.perform(req, res) do |prev_req, _|
436
+ expect(prev_req.body).to eq(post_body)
437
+ expect(prev_req.headers["Cookie"]).to eq(cookie)
438
+ expect(prev_req.headers["Content-Type"]).to eq("meme")
439
+ simple_response 200
440
+ end
441
+ end
442
+ end
399
443
  end
400
444
  end
@@ -47,9 +47,20 @@ RSpec.describe HTTP::Request::Writer do
47
47
  end
48
48
  end
49
49
 
50
- context "when body is empty" do
50
+ context "when body is not set" do
51
51
  let(:body) { HTTP::Request::Body.new(nil) }
52
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
+
53
64
  it "doesn't write anything to the socket and sets Content-Length" do
54
65
  writer.stream
55
66
  expect(io.string).to eq [
@@ -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) { [String.new("Hello, "), String.new("World!")] }
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) { [String.new("")] }
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").force_encoding(Encoding::UTF_8))
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").force_encoding(Encoding::BINARY))
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
- [String.new(body[0, len / 2]), String.new(body[(len / 2)..-1])]
62
+ [body[0, len / 2], body[(len / 2)..-1]]
63
63
  end
64
64
  subject(:body) do
65
65
  inflater = HTTP::Response::Inflater.new(connection)
@@ -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.split(//) }
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
@@ -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,8 +12,7 @@ RSpec.describe HTTP::Response do
11
12
  :version => "1.1",
12
13
  :headers => headers,
13
14
  :body => body,
14
- :uri => uri,
15
- :request => HTTP::Request.new(:verb => :get, :uri => "http://example.com")
15
+ :request => request
16
16
  )
17
17
  end
18
18
 
@@ -87,19 +87,32 @@ RSpec.describe HTTP::Response do
87
87
  end
88
88
 
89
89
  describe "#parse" do
90
- let(:headers) { {"Content-Type" => "application/json"} }
90
+ let(:headers) { {"Content-Type" => content_type} }
91
91
  let(:body) { '{"foo":"bar"}' }
92
92
 
93
- it "fails if MIME type decoder is not found" do
94
- expect { response.parse "text/html" }.to raise_error(HTTP::Error)
93
+ context "with known content type" do
94
+ let(:content_type) { "application/json" }
95
+ it "returns parsed body" do
96
+ expect(response.parse).to eq "foo" => "bar"
97
+ end
95
98
  end
96
99
 
97
- it "uses decoder found by given MIME type" do
98
- expect(response.parse("application/json")).to eq("foo" => "bar")
100
+ context "with unknown content type" do
101
+ let(:content_type) { "application/deadbeef" }
102
+ it "raises HTTP::Error" do
103
+ expect { response.parse }.to raise_error HTTP::Error
104
+ end
99
105
  end
100
106
 
101
- it "uses decoder found by given MIME type alias" do
102
- expect(response.parse(:json)).to eq("foo" => "bar")
107
+ context "with explicitly given mime type" do
108
+ let(:content_type) { "application/deadbeef" }
109
+ it "ignores mime_type of response" do
110
+ expect(response.parse("application/json")).to eq "foo" => "bar"
111
+ end
112
+
113
+ it "supports mime type aliases" do
114
+ expect(response.parse(:json)).to eq "foo" => "bar"
115
+ end
103
116
  end
104
117
  end
105
118
 
@@ -154,7 +167,7 @@ RSpec.describe HTTP::Response do
154
167
  :version => "1.1",
155
168
  :status => 200,
156
169
  :connection => connection,
157
- :request => HTTP::Request.new(:verb => :get, :uri => "http://example.com")
170
+ :request => request
158
171
  )
159
172
  end
160
173
 
@@ -171,4 +184,43 @@ RSpec.describe HTTP::Response do
171
184
  end
172
185
  it { is_expected.not_to be_chunked }
173
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
174
226
  end
@@ -95,7 +95,8 @@ RSpec.describe HTTP do
95
95
  expect(response.to_s).to match(/<!doctype html>/)
96
96
  end
97
97
 
98
- context "ssl" do
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
- context "ssl" do
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>/)
@@ -438,6 +440,22 @@ RSpec.describe HTTP do
438
440
 
439
441
  expect(response.to_s).to eq("#{body}-deflated")
440
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
441
459
  end
442
460
 
443
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
- require "simplecov"
4
- require "coveralls"
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.