http 3.3.0 → 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +3 -1
  4. data/.travis.yml +10 -7
  5. data/CHANGES.md +135 -0
  6. data/README.md +14 -10
  7. data/Rakefile +1 -1
  8. data/http.gemspec +12 -5
  9. data/lib/http.rb +1 -2
  10. data/lib/http/chainable.rb +20 -29
  11. data/lib/http/client.rb +25 -19
  12. data/lib/http/connection.rb +5 -9
  13. data/lib/http/feature.rb +14 -0
  14. data/lib/http/features/auto_deflate.rb +27 -6
  15. data/lib/http/features/auto_inflate.rb +33 -6
  16. data/lib/http/features/instrumentation.rb +64 -0
  17. data/lib/http/features/logging.rb +55 -0
  18. data/lib/http/features/normalize_uri.rb +17 -0
  19. data/lib/http/headers/known.rb +3 -0
  20. data/lib/http/options.rb +27 -21
  21. data/lib/http/redirector.rb +2 -1
  22. data/lib/http/request.rb +38 -30
  23. data/lib/http/request/body.rb +30 -1
  24. data/lib/http/request/writer.rb +21 -7
  25. data/lib/http/response.rb +7 -15
  26. data/lib/http/response/parser.rb +56 -16
  27. data/lib/http/timeout/global.rb +12 -14
  28. data/lib/http/timeout/per_operation.rb +5 -7
  29. data/lib/http/uri.rb +13 -0
  30. data/lib/http/version.rb +1 -1
  31. data/spec/lib/http/client_spec.rb +34 -7
  32. data/spec/lib/http/features/auto_inflate_spec.rb +38 -22
  33. data/spec/lib/http/features/instrumentation_spec.rb +56 -0
  34. data/spec/lib/http/features/logging_spec.rb +67 -0
  35. data/spec/lib/http/redirector_spec.rb +13 -0
  36. data/spec/lib/http/request/body_spec.rb +51 -0
  37. data/spec/lib/http/request/writer_spec.rb +20 -0
  38. data/spec/lib/http/request_spec.rb +6 -0
  39. data/spec/lib/http/response/parser_spec.rb +45 -0
  40. data/spec/lib/http/response_spec.rb +3 -4
  41. data/spec/lib/http_spec.rb +45 -65
  42. data/spec/regression_specs.rb +7 -0
  43. data/spec/support/dummy_server/servlet.rb +5 -0
  44. data/spec/support/http_handling_shared.rb +60 -64
  45. metadata +32 -21
  46. data/.ruby-version +0 -1
@@ -74,5 +74,25 @@ RSpec.describe HTTP::Request::Writer do
74
74
  ].join
75
75
  end
76
76
  end
77
+
78
+ context "when server won't accept any more data" do
79
+ before do
80
+ expect(io).to receive(:write).and_raise(Errno::EPIPE)
81
+ end
82
+
83
+ it "aborts silently" do
84
+ writer.stream
85
+ end
86
+ end
87
+
88
+ context "when writing to socket raises an exception" do
89
+ before do
90
+ expect(io).to receive(:write).and_raise(Errno::ECONNRESET)
91
+ end
92
+
93
+ it "raises a ConnectionError" do
94
+ expect { writer.stream }.to raise_error(HTTP::ConnectionError)
95
+ end
96
+ end
77
97
  end
78
98
  end
@@ -225,4 +225,10 @@ RSpec.describe HTTP::Request do
225
225
  end
226
226
  end
227
227
  end
228
+
229
+ describe "#inspect" do
230
+ subject { request.inspect }
231
+
232
+ it { is_expected.to eq "#<HTTP::Request/1.1 GET #{request_uri}>" }
233
+ end
228
234
  end
@@ -0,0 +1,45 @@
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\nMy-Header: val\r\nEmpty-Header: \r\n\r\n{}"
7
+ end
8
+ let(:expected_headers) do
9
+ {
10
+ "Content-Length" => "2",
11
+ "Content-Type" => "application/json",
12
+ "My-Header" => "val",
13
+ "Empty-Header" => ""
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.split(//) }
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
+ end
@@ -129,13 +129,12 @@ RSpec.describe HTTP::Response do
129
129
  end
130
130
 
131
131
  describe "#inspect" do
132
+ subject { response.inspect }
133
+
132
134
  let(:headers) { {:content_type => "text/plain"} }
133
135
  let(:body) { double :to_s => "foobar" }
134
136
 
135
- it "returns human-friendly response representation" do
136
- expect(response.inspect).
137
- to eq '#<HTTP::Response/1.1 200 OK {"Content-Type"=>"text/plain"}>'
138
- end
137
+ it { is_expected.to eq '#<HTTP::Response/1.1 200 OK {"Content-Type"=>"text/plain"}>' }
139
138
  end
140
139
 
141
140
  describe "#cookies" do
@@ -55,38 +55,17 @@ RSpec.describe HTTP do
55
55
  end
56
56
 
57
57
  context "with a large request body" do
58
- %w[global null per_operation].each do |timeout|
59
- context "with a #{timeout} timeout" do
60
- [16_000, 16_500, 17_000, 34_000, 68_000].each do |size|
61
- [0, rand(0..100), rand(100..1000)].each do |fuzzer|
62
- context "with a #{size} body and #{fuzzer} of fuzzing" do
63
- let(:client) { HTTP.timeout(timeout, :read => 2, :write => 2, :connect => 2) }
64
-
65
- let(:characters) { ("A".."Z").to_a }
66
- let(:request_body) do
67
- Array.new(size + fuzzer) { |i| characters[i % characters.length] }.join
68
- end
69
-
70
- it "returns a large body" do
71
- response = client.post("#{dummy.endpoint}/echo-body", :body => request_body)
72
-
73
- expect(response.body.to_s).to eq(request_body)
74
- expect(response.headers["Content-Length"].to_i).to eq(request_body.bytesize)
75
- end
76
-
77
- context "when bytesize != length" do
78
- let(:characters) { ("A".."Z").to_a.push("“") }
79
-
80
- it "returns a large body" do
81
- body = {:data => request_body}
82
- response = client.post("#{dummy.endpoint}/echo-body", :json => body)
83
-
84
- expect(CGI.unescape(response.body.to_s)).to eq(body.to_json)
85
- expect(response.headers["Content-Length"].to_i).to eq(body.to_json.bytesize)
86
- end
87
- end
88
- end
89
- end
58
+ let(:request_body) { "“" * 1_000_000 } # use multi-byte character
59
+
60
+ [:null, 6, {:read => 2, :write => 2, :connect => 2}].each do |timeout|
61
+ context "with `.timeout(#{timeout.inspect})`" do
62
+ let(:client) { HTTP.timeout(timeout) }
63
+
64
+ it "writes the whole body" do
65
+ response = client.post "#{dummy.endpoint}/echo-body", :body => request_body
66
+
67
+ expect(response.body.to_s).to eq(request_body.b)
68
+ expect(response.headers["Content-Length"].to_i).to eq request_body.bytesize
90
69
  end
91
70
  end
92
71
  end
@@ -194,7 +173,7 @@ RSpec.describe HTTP do
194
173
 
195
174
  context "with encoding option" do
196
175
  it "respects option" do
197
- response = HTTP.get "#{dummy.endpoint}/iso-8859-1", "encoding" => Encoding::BINARY
176
+ response = HTTP.get "#{dummy.endpoint}/iso-8859-1", :encoding => Encoding::BINARY
198
177
  expect(response.to_s.encoding).to eq(Encoding::BINARY)
199
178
  end
200
179
  end
@@ -202,7 +181,7 @@ RSpec.describe HTTP do
202
181
 
203
182
  context "passing a string encoding type" do
204
183
  it "finds encoding" do
205
- response = HTTP.get dummy.endpoint, "encoding" => "ascii"
184
+ response = HTTP.get dummy.endpoint, :encoding => "ascii"
206
185
  expect(response.to_s.encoding).to eq(Encoding::ASCII)
207
186
  end
208
187
  end
@@ -255,15 +234,15 @@ RSpec.describe HTTP do
255
234
 
256
235
  describe ".basic_auth" do
257
236
  it "fails when options is not a Hash" do
258
- expect { HTTP.basic_auth "[FOOBAR]" }.to raise_error
237
+ expect { HTTP.basic_auth "[FOOBAR]" }.to raise_error(NoMethodError)
259
238
  end
260
239
 
261
240
  it "fails when :pass is not given" do
262
- expect { HTTP.basic_auth :user => "[USER]" }.to raise_error
241
+ expect { HTTP.basic_auth :user => "[USER]" }.to raise_error(KeyError)
263
242
  end
264
243
 
265
244
  it "fails when :user is not given" do
266
- expect { HTTP.basic_auth :pass => "[PASS]" }.to raise_error
245
+ expect { HTTP.basic_auth :pass => "[PASS]" }.to raise_error(KeyError)
267
246
  end
268
247
 
269
248
  it "sets Authorization header with proper BasicAuth value" do
@@ -305,22 +284,8 @@ RSpec.describe HTTP do
305
284
  end
306
285
 
307
286
  describe ".timeout" do
308
- context "without timeout type" do
309
- subject(:client) { HTTP.timeout :read => 123 }
310
-
311
- it "sets timeout_class to PerOperation" do
312
- expect(client.default_options.timeout_class).
313
- to be HTTP::Timeout::PerOperation
314
- end
315
-
316
- it "sets given timeout options" do
317
- expect(client.default_options.timeout_options).
318
- to eq :read_timeout => 123
319
- end
320
- end
321
-
322
- context "with :null type" do
323
- subject(:client) { HTTP.timeout :null, :read => 123 }
287
+ context "specifying a null timeout" do
288
+ subject(:client) { HTTP.timeout :null }
324
289
 
325
290
  it "sets timeout_class to Null" do
326
291
  expect(client.default_options.timeout_class).
@@ -328,8 +293,8 @@ RSpec.describe HTTP do
328
293
  end
329
294
  end
330
295
 
331
- context "with :per_operation type" do
332
- subject(:client) { HTTP.timeout :per_operation, :read => 123 }
296
+ context "specifying per operation timeouts" do
297
+ subject(:client) { HTTP.timeout :read => 123 }
333
298
 
334
299
  it "sets timeout_class to PerOperation" do
335
300
  expect(client.default_options.timeout_class).
@@ -342,24 +307,19 @@ RSpec.describe HTTP do
342
307
  end
343
308
  end
344
309
 
345
- context "with :global type" do
346
- subject(:client) { HTTP.timeout :global, :read => 123 }
310
+ context "specifying a global timeout" do
311
+ subject(:client) { HTTP.timeout 123 }
347
312
 
348
313
  it "sets timeout_class to Global" do
349
314
  expect(client.default_options.timeout_class).
350
315
  to be HTTP::Timeout::Global
351
316
  end
352
317
 
353
- it "sets given timeout options" do
318
+ it "sets given timeout option" do
354
319
  expect(client.default_options.timeout_options).
355
- to eq :read_timeout => 123
320
+ to eq :global_timeout => 123
356
321
  end
357
322
  end
358
-
359
- it "fails with unknown timeout type" do
360
- expect { HTTP.timeout(:foobar, :read => 123) }.
361
- to raise_error(ArgumentError, /foobar/)
362
- end
363
323
  end
364
324
 
365
325
  describe ".cookies" do
@@ -470,13 +430,33 @@ RSpec.describe HTTP do
470
430
  expect(response.to_s).to eq("#{body}-deflated")
471
431
  end
472
432
  end
433
+
434
+ context "with :normalize_uri" do
435
+ it "normalizes URI" do
436
+ response = HTTP.get "#{dummy.endpoint}/hello world"
437
+ expect(response.to_s).to eq("hello world")
438
+ end
439
+
440
+ it "uses the custom URI Normalizer method" do
441
+ client = HTTP.use(:normalize_uri => {:normalizer => :itself.to_proc})
442
+ response = client.get("#{dummy.endpoint}/hello world")
443
+ expect(response.status).to eq(400)
444
+ end
445
+
446
+ it "uses the default URI normalizer" do
447
+ client = HTTP.use :normalize_uri
448
+ expect(HTTP::URI::NORMALIZER).to receive(:call).and_call_original
449
+ response = client.get("#{dummy.endpoint}/hello world")
450
+ expect(response.to_s).to eq("hello world")
451
+ end
452
+ end
473
453
  end
474
454
 
475
455
  it "unifies socket errors into HTTP::ConnectionError" do
476
456
  expect { HTTP.get "http://thishostshouldnotexists.com" }.
477
457
  to raise_error HTTP::ConnectionError
478
458
 
479
- expect { HTTP.get "http://127.0.0.1:000" }.
459
+ expect { HTTP.get "http://127.0.0.1:111" }.
480
460
  to raise_error HTTP::ConnectionError
481
461
  end
482
462
  end
@@ -14,4 +14,11 @@ RSpec.describe "Regression testing" do
14
14
  expect { HTTP.get(google_uri).to_s }.not_to raise_error
15
15
  end
16
16
  end
17
+
18
+ describe "#422" do
19
+ it "reads body when 200 OK response contains Upgrade header" do
20
+ res = HTTP.get("https://httpbin.org/response-headers?Upgrade=h2,h2c")
21
+ expect(res.parse(:json)).to include("Upgrade" => "h2,h2c")
22
+ end
23
+ end
17
24
  end
@@ -148,6 +148,11 @@ class DummyServer < WEBrick::HTTPServer
148
148
  res.body = req.body
149
149
  end
150
150
 
151
+ get "/hello world" do |_req, res|
152
+ res.status = 200
153
+ res.body = "hello world"
154
+ end
155
+
151
156
  post "/encoded-body" do |req, res|
152
157
  res.status = 200
153
158
 
@@ -1,14 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  RSpec.shared_context "HTTP handling" do
4
- describe "timeouts" do
5
- let(:conn_timeout) { 1 }
6
- let(:read_timeout) { 1 }
7
- let(:write_timeout) { 1 }
4
+ context "without timeouts" do
5
+ let(:options) { {:timeout_class => HTTP::Timeout::Null, :timeout_options => {}} }
6
+
7
+ it "works" do
8
+ expect(client.get(server.endpoint).body.to_s).to eq("<!doctype html>")
9
+ end
10
+ end
11
+
12
+ context "with a per operation timeout" do
13
+ let(:response) { client.get(server.endpoint).body.to_s }
8
14
 
9
15
  let(:options) do
10
16
  {
11
- :timeout_class => timeout_class,
17
+ :timeout_class => HTTP::Timeout::PerOperation,
12
18
  :timeout_options => {
13
19
  :connect_timeout => conn_timeout,
14
20
  :read_timeout => read_timeout,
@@ -16,87 +22,77 @@ RSpec.shared_context "HTTP handling" do
16
22
  }
17
23
  }
18
24
  end
25
+ let(:conn_timeout) { 1 }
26
+ let(:read_timeout) { 1 }
27
+ let(:write_timeout) { 1 }
19
28
 
20
- context "without timeouts" do
21
- let(:timeout_class) { HTTP::Timeout::Null }
22
- let(:conn_timeout) { 0 }
23
- let(:read_timeout) { 0 }
24
- let(:write_timeout) { 0 }
25
-
26
- it "works" do
27
- expect(client.get(server.endpoint).body.to_s).to eq("<!doctype html>")
28
- end
29
+ it "works" do
30
+ expect(response).to eq("<!doctype html>")
29
31
  end
30
32
 
31
- context "with a per operation timeout" do
32
- let(:timeout_class) { HTTP::Timeout::PerOperation }
33
-
34
- let(:response) { client.get(server.endpoint).body.to_s }
35
-
36
- it "works" do
37
- expect(response).to eq("<!doctype html>")
38
- end
33
+ context "connection" do
34
+ context "of 1" do
35
+ let(:conn_timeout) { 1 }
39
36
 
40
- context "connection" do
41
- context "of 1" do
42
- let(:conn_timeout) { 1 }
43
-
44
- it "does not time out" do
45
- expect { response }.to_not raise_error
46
- end
37
+ it "does not time out" do
38
+ expect { response }.to_not raise_error
47
39
  end
48
40
  end
41
+ end
49
42
 
50
- context "read" do
51
- context "of 0" do
52
- let(:read_timeout) { 0 }
43
+ context "read" do
44
+ context "of 0" do
45
+ let(:read_timeout) { 0 }
53
46
 
54
- it "times out", :flaky do
55
- expect { response }.to raise_error(HTTP::TimeoutError, /Read/i)
56
- end
47
+ it "times out", :flaky do
48
+ expect { response }.to raise_error(HTTP::TimeoutError, /Read/i)
57
49
  end
50
+ end
58
51
 
59
- context "of 2.5" do
60
- let(:read_timeout) { 2.5 }
52
+ context "of 2.5" do
53
+ let(:read_timeout) { 2.5 }
61
54
 
62
- it "does not time out", :flaky do
63
- expect { client.get("#{server.endpoint}/sleep").body.to_s }.to_not raise_error
64
- end
55
+ it "does not time out", :flaky do
56
+ expect { client.get("#{server.endpoint}/sleep").body.to_s }.to_not raise_error
65
57
  end
66
58
  end
67
59
  end
60
+ end
68
61
 
69
- context "with a global timeout" do
70
- let(:timeout_class) { HTTP::Timeout::Global }
71
-
72
- let(:conn_timeout) { 0 }
73
- let(:read_timeout) { 1 }
74
- let(:write_timeout) { 0 }
75
-
76
- let(:response) { client.get(server.endpoint).body.to_s }
62
+ context "with a global timeout" do
63
+ let(:options) do
64
+ {
65
+ :timeout_class => HTTP::Timeout::Global,
66
+ :timeout_options => {
67
+ :global_timeout => global_timeout
68
+ }
69
+ }
70
+ end
71
+ let(:global_timeout) { 1 }
77
72
 
78
- it "errors if connecting takes too long" do
79
- expect(TCPSocket).to receive(:open) do
80
- sleep 1.25
81
- end
73
+ let(:response) { client.get(server.endpoint).body.to_s }
82
74
 
83
- expect { response }.to raise_error(HTTP::TimeoutError, /execution/)
75
+ it "errors if connecting takes too long" do
76
+ expect(TCPSocket).to receive(:open) do
77
+ sleep 1.25
84
78
  end
85
79
 
86
- it "errors if reading takes too long" do
87
- expect { client.get("#{server.endpoint}/sleep").body.to_s }.
88
- to raise_error(HTTP::TimeoutError, /Timed out/)
89
- end
80
+ expect { response }.to raise_error(HTTP::TimeoutError, /execution/)
81
+ end
90
82
 
91
- context "it resets state when reusing connections" do
92
- let(:extra_options) { {:persistent => server.endpoint} }
83
+ it "errors if reading takes too long" do
84
+ expect { client.get("#{server.endpoint}/sleep").body.to_s }.
85
+ to raise_error(HTTP::TimeoutError, /Timed out/)
86
+ end
93
87
 
94
- let(:read_timeout) { 2.5 }
88
+ context "it resets state when reusing connections" do
89
+ let(:extra_options) { {:persistent => server.endpoint} }
95
90
 
96
- it "does not timeout", :flaky do
97
- client.get("#{server.endpoint}/sleep").body.to_s
98
- client.get("#{server.endpoint}/sleep").body.to_s
99
- end
91
+ let(:global_timeout) { 2.5 }
92
+
93
+ it "does not timeout", :flaky do
94
+ client.get("#{server.endpoint}/sleep").body.to_s
95
+ client.get("#{server.endpoint}/sleep").body.to_s
100
96
  end
101
97
  end
102
98
  end