http 4.4.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) 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.yml +8 -110
  6. data/.rubocop/layout.yml +8 -0
  7. data/.rubocop/style.yml +32 -0
  8. data/.rubocop_todo.yml +192 -0
  9. data/.yardopts +1 -1
  10. data/CHANGES.md +90 -0
  11. data/Gemfile +18 -10
  12. data/README.md +17 -20
  13. data/Rakefile +2 -10
  14. data/http.gemspec +3 -3
  15. data/lib/http/chainable.rb +23 -17
  16. data/lib/http/client.rb +36 -30
  17. data/lib/http/connection.rb +11 -7
  18. data/lib/http/content_type.rb +12 -7
  19. data/lib/http/feature.rb +3 -1
  20. data/lib/http/features/auto_deflate.rb +6 -6
  21. data/lib/http/features/auto_inflate.rb +6 -5
  22. data/lib/http/features/instrumentation.rb +1 -1
  23. data/lib/http/features/logging.rb +19 -21
  24. data/lib/http/headers.rb +50 -13
  25. data/lib/http/mime_type/adapter.rb +3 -1
  26. data/lib/http/mime_type/json.rb +1 -0
  27. data/lib/http/options.rb +5 -8
  28. data/lib/http/redirector.rb +2 -1
  29. data/lib/http/request.rb +13 -10
  30. data/lib/http/request/body.rb +1 -0
  31. data/lib/http/request/writer.rb +3 -2
  32. data/lib/http/response.rb +17 -15
  33. data/lib/http/response/body.rb +2 -2
  34. data/lib/http/response/inflater.rb +1 -1
  35. data/lib/http/response/parser.rb +75 -49
  36. data/lib/http/response/status.rb +4 -3
  37. data/lib/http/timeout/global.rb +17 -31
  38. data/lib/http/timeout/null.rb +2 -1
  39. data/lib/http/timeout/per_operation.rb +31 -54
  40. data/lib/http/uri.rb +5 -5
  41. data/lib/http/version.rb +1 -1
  42. data/spec/lib/http/client_spec.rb +119 -30
  43. data/spec/lib/http/connection_spec.rb +8 -5
  44. data/spec/lib/http/features/auto_inflate_spec.rb +4 -2
  45. data/spec/lib/http/features/instrumentation_spec.rb +28 -21
  46. data/spec/lib/http/features/logging_spec.rb +8 -9
  47. data/spec/lib/http/headers_spec.rb +53 -18
  48. data/spec/lib/http/options/headers_spec.rb +1 -1
  49. data/spec/lib/http/options/merge_spec.rb +16 -16
  50. data/spec/lib/http/redirector_spec.rb +2 -1
  51. data/spec/lib/http/request/writer_spec.rb +13 -1
  52. data/spec/lib/http/request_spec.rb +5 -5
  53. data/spec/lib/http/response/parser_spec.rb +74 -0
  54. data/spec/lib/http/response/status_spec.rb +3 -3
  55. data/spec/lib/http/response_spec.rb +11 -22
  56. data/spec/lib/http_spec.rb +30 -3
  57. data/spec/spec_helper.rb +21 -21
  58. data/spec/support/black_hole.rb +1 -1
  59. data/spec/support/dummy_server.rb +7 -7
  60. data/spec/support/dummy_server/servlet.rb +17 -6
  61. data/spec/support/fuubar.rb +21 -0
  62. data/spec/support/http_handling_shared.rb +4 -4
  63. data/spec/support/simplecov.rb +19 -0
  64. data/spec/support/ssl_helper.rb +4 -4
  65. metadata +21 -14
  66. data/.coveralls.yml +0 -1
  67. 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 = 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
@@ -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 => :post,
21
- :uri => "https://example.com/",
22
- :headers => {:accept => "application/json"},
23
- :body => '{"hello": "world!"}'
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 "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,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("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
@@ -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,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
 
@@ -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 => :get,
12
- :uri => request_uri,
13
- :headers => headers,
14
- :proxy => 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" => content_type} }
90
+ let(:headers) { {"Content-Type" => "application/json"} }
90
91
  let(:body) { '{"foo":"bar"}' }
91
92
 
92
- context "with known content type" do
93
- let(:content_type) { "application/json" }
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
- context "with unknown content type" do
100
- let(:content_type) { "application/deadbeef" }
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
- context "with explicitly given mime type" do
107
- let(:content_type) { "application/deadbeef" }
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