http 3.3.0 → 4.4.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 (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