http 5.0.0.pre3 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) 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 +7 -124
  6. data/.rubocop/layout.yml +8 -0
  7. data/.rubocop/style.yml +32 -0
  8. data/.rubocop_todo.yml +192 -0
  9. data/CHANGES.md +48 -1
  10. data/Gemfile +18 -11
  11. data/README.md +13 -17
  12. data/Rakefile +2 -10
  13. data/http.gemspec +2 -2
  14. data/lib/http/chainable.rb +15 -14
  15. data/lib/http/client.rb +18 -11
  16. data/lib/http/connection.rb +7 -3
  17. data/lib/http/content_type.rb +10 -5
  18. data/lib/http/feature.rb +1 -1
  19. data/lib/http/features/instrumentation.rb +1 -1
  20. data/lib/http/features/logging.rb +19 -21
  21. data/lib/http/headers.rb +3 -3
  22. data/lib/http/mime_type/adapter.rb +2 -0
  23. data/lib/http/options.rb +2 -2
  24. data/lib/http/redirector.rb +1 -1
  25. data/lib/http/request.rb +7 -4
  26. data/lib/http/response/body.rb +1 -2
  27. data/lib/http/response/inflater.rb +1 -1
  28. data/lib/http/response/parser.rb +72 -64
  29. data/lib/http/response/status.rb +2 -2
  30. data/lib/http/timeout/global.rb +16 -28
  31. data/lib/http/timeout/null.rb +2 -1
  32. data/lib/http/timeout/per_operation.rb +31 -55
  33. data/lib/http/version.rb +1 -1
  34. data/spec/lib/http/client_spec.rb +75 -41
  35. data/spec/lib/http/features/instrumentation_spec.rb +21 -15
  36. data/spec/lib/http/features/logging_spec.rb +2 -4
  37. data/spec/lib/http/headers_spec.rb +3 -3
  38. data/spec/lib/http/response/parser_spec.rb +2 -2
  39. data/spec/lib/http_spec.rb +20 -2
  40. data/spec/spec_helper.rb +21 -21
  41. data/spec/support/black_hole.rb +1 -1
  42. data/spec/support/dummy_server.rb +1 -1
  43. data/spec/support/dummy_server/servlet.rb +14 -2
  44. data/spec/support/fuubar.rb +21 -0
  45. data/spec/support/simplecov.rb +19 -0
  46. metadata +21 -16
  47. data/.coveralls.yml +0 -1
  48. data/.travis.yml +0 -38
@@ -12,7 +12,7 @@ module HTTP
12
12
 
13
13
  attr_reader :options, :socket
14
14
 
15
- def initialize(options = {}) # rubocop:disable Style/OptionHash
15
+ def initialize(options = {})
16
16
  @options = options
17
17
  end
18
18
 
@@ -36,6 +36,7 @@ module HTTP
36
36
  connect_ssl
37
37
 
38
38
  return unless ssl_context.verify_mode == OpenSSL::SSL::VERIFY_PEER
39
+ return if ssl_context.respond_to?(:verify_hostname) && !ssl_context.verify_hostname
39
40
 
40
41
  @socket.post_connection_check(host)
41
42
  end
@@ -34,66 +34,42 @@ module HTTP
34
34
  end
35
35
  end
36
36
 
37
- # NIO with exceptions
38
- if RUBY_VERSION < "2.1.0"
39
- # Read data from the socket
40
- def readpartial(size, buffer = nil)
41
- rescue_readable do
42
- @socket.read_nonblock(size, buffer)
43
- end
44
- rescue EOFError
45
- :eof
46
- end
47
-
48
- # Write data to the socket
49
- def write(data)
50
- rescue_writable do
51
- @socket.write_nonblock(data)
52
- end
53
- rescue EOFError
54
- :eof
55
- end
56
-
57
- # NIO without exceptions
58
- else
59
- # Read data from the socket
60
- def readpartial(size, buffer = nil)
61
- timeout = false
62
- loop do
63
- result = @socket.read_nonblock(size, buffer, :exception => false)
64
-
65
- return :eof if result.nil?
66
- return result if result != :wait_readable
67
-
68
- raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
69
-
70
- # marking the socket for timeout. Why is this not being raised immediately?
71
- # it seems there is some race-condition on the network level between calling
72
- # #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
73
- # for reads, and when waiting for x seconds, it returns nil suddenly without completing
74
- # the x seconds. In a normal case this would be a timeout on wait/read, but it can
75
- # also mean that the socket has been closed by the server. Therefore we "mark" the
76
- # socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
77
- # timeout. Else, the first timeout was a proper timeout.
78
- # This hack has to be done because io/wait#wait_readable doesn't provide a value for when
79
- # the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
80
- timeout = true unless @socket.to_io.wait_readable(@read_timeout)
81
- end
37
+ # Read data from the socket
38
+ def readpartial(size, buffer = nil)
39
+ timeout = false
40
+ loop do
41
+ result = @socket.read_nonblock(size, buffer, :exception => false)
42
+
43
+ return :eof if result.nil?
44
+ return result if result != :wait_readable
45
+
46
+ raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
47
+
48
+ # marking the socket for timeout. Why is this not being raised immediately?
49
+ # it seems there is some race-condition on the network level between calling
50
+ # #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
51
+ # for reads, and when waiting for x seconds, it returns nil suddenly without completing
52
+ # the x seconds. In a normal case this would be a timeout on wait/read, but it can
53
+ # also mean that the socket has been closed by the server. Therefore we "mark" the
54
+ # socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
55
+ # timeout. Else, the first timeout was a proper timeout.
56
+ # This hack has to be done because io/wait#wait_readable doesn't provide a value for when
57
+ # the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
58
+ timeout = true unless @socket.to_io.wait_readable(@read_timeout)
82
59
  end
60
+ end
83
61
 
84
- # Write data to the socket
85
- def write(data)
86
- timeout = false
87
- loop do
88
- result = @socket.write_nonblock(data, :exception => false)
89
- return result unless result == :wait_writable
62
+ # Write data to the socket
63
+ def write(data)
64
+ timeout = false
65
+ loop do
66
+ result = @socket.write_nonblock(data, :exception => false)
67
+ return result unless result == :wait_writable
90
68
 
91
- raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
69
+ raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
92
70
 
93
- timeout = true unless @socket.to_io.wait_writable(@write_timeout)
94
- end
71
+ timeout = true unless @socket.to_io.wait_writable(@write_timeout)
95
72
  end
96
-
97
73
  end
98
74
  end
99
75
  end
data/lib/http/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- VERSION = "5.0.0.pre3"
4
+ VERSION = "5.0.0"
5
5
  end
@@ -8,46 +8,50 @@ require "support/ssl_helper"
8
8
  RSpec.describe HTTP::Client do
9
9
  run_server(:dummy) { DummyServer.new }
10
10
 
11
- StubbedClient = Class.new(HTTP::Client) do
12
- def perform(request, options)
13
- stubbed = stubs[request.uri]
14
- stubbed ? stubbed.call(request) : super(request, options)
15
- end
16
-
17
- def stubs
18
- @stubs ||= {}
19
- end
11
+ before do
12
+ stubbed_client = Class.new(HTTP::Client) do
13
+ def perform(request, options)
14
+ stubbed = stubs[HTTP::URI::NORMALIZER.call(request.uri).to_s]
15
+ stubbed ? stubbed.call(request) : super(request, options)
16
+ end
20
17
 
21
- def stub(stubs)
22
- @stubs = stubs.each_with_object({}) do |(k, v), o|
23
- o[HTTP::URI.parse k] = v
18
+ def stubs
19
+ @stubs ||= {}
24
20
  end
25
21
 
26
- self
22
+ def stub(stubs)
23
+ @stubs = stubs.transform_keys do |k|
24
+ HTTP::URI::NORMALIZER.call(k).to_s
25
+ end
26
+
27
+ self
28
+ end
27
29
  end
28
- end
29
30
 
30
- def redirect_response(location, status = 302)
31
- lambda do |request|
32
- HTTP::Response.new(
33
- :status => status,
34
- :version => "1.1",
35
- :headers => {"Location" => location},
36
- :body => "",
37
- :request => request
38
- )
31
+ def redirect_response(location, status = 302)
32
+ lambda do |request|
33
+ HTTP::Response.new(
34
+ :status => status,
35
+ :version => "1.1",
36
+ :headers => {"Location" => location},
37
+ :body => "",
38
+ :request => request
39
+ )
40
+ end
39
41
  end
40
- end
41
42
 
42
- def simple_response(body, status = 200)
43
- lambda do |request|
44
- HTTP::Response.new(
45
- :status => status,
46
- :version => "1.1",
47
- :body => body,
48
- :request => request
49
- )
43
+ def simple_response(body, status = 200)
44
+ lambda do |request|
45
+ HTTP::Response.new(
46
+ :status => status,
47
+ :version => "1.1",
48
+ :body => body,
49
+ :request => request
50
+ )
51
+ end
50
52
  end
53
+
54
+ stub_const("StubbedClient", stubbed_client)
51
55
  end
52
56
 
53
57
  describe "following redirects" do
@@ -105,9 +109,8 @@ RSpec.describe HTTP::Client do
105
109
  end
106
110
 
107
111
  it "works like a charm in real world" do
108
- url = "http://git.io/jNeY"
109
- client = HTTP.follow
110
- expect(client.get(url).to_s).to include "support for non-ascii URIs"
112
+ expect(HTTP.follow.get("https://bit.ly/2UaBT4R").parse(:json)).
113
+ to include("url" => "https://httpbin.org/anything/könig")
111
114
  end
112
115
  end
113
116
  end
@@ -197,6 +200,22 @@ RSpec.describe HTTP::Client do
197
200
 
198
201
  client.get("http://example.com/", :form => {:foo => HTTP::FormData::Part.new("content")})
199
202
  end
203
+
204
+ context "when passing an HTTP::FormData object directly" do
205
+ it "creates url encoded form data object" do
206
+ client = HTTP::Client.new
207
+ form_data = HTTP::FormData::Multipart.new({ :foo => "bar" })
208
+
209
+ allow(client).to receive(:perform)
210
+
211
+ expect(HTTP::Request).to receive(:new) do |opts|
212
+ expect(opts[:body]).to be form_data
213
+ expect(opts[:body].to_s).to match(/^Content-Disposition: form-data; name="foo"\r\n\r\nbar\r\n/m)
214
+ end
215
+
216
+ client.get("http://example.com/", :form => form_data)
217
+ end
218
+ end
200
219
  end
201
220
 
202
221
  describe "passing json" do
@@ -220,9 +239,9 @@ RSpec.describe HTTP::Client do
220
239
  end
221
240
 
222
241
  it "works like a charm in real world" do
223
- url = "https://github.com/httprb/http.rb/pull/197/ö無"
224
- client = HTTP.follow
225
- expect(client.get(url).to_s).to include "support for non-ascii URIs"
242
+ url = "https://httpbin.org/anything/ö無"
243
+
244
+ expect(HTTP.follow.get(url).parse(:json)).to include("url" => url)
226
245
  end
227
246
  end
228
247
 
@@ -291,6 +310,7 @@ RSpec.describe HTTP::Client do
291
310
  end
292
311
  end
293
312
  end
313
+
294
314
  it "is given a chance to wrap the Request" do
295
315
  feature_instance = feature_class.new
296
316
 
@@ -299,7 +319,7 @@ RSpec.describe HTTP::Client do
299
319
 
300
320
  expect(response.code).to eq(200)
301
321
  expect(feature_instance.captured_request.verb).to eq(:get)
302
- expect(feature_instance.captured_request.uri.to_s).to eq(dummy.endpoint + "/")
322
+ expect(feature_instance.captured_request.uri.to_s).to eq("#{dummy.endpoint}/")
303
323
  end
304
324
 
305
325
  it "is given a chance to wrap the Response" do
@@ -325,6 +345,19 @@ RSpec.describe HTTP::Client do
325
345
  expect(feature_instance.captured_request.verb).to eq(:post)
326
346
  expect(feature_instance.captured_request.uri.to_s).to eq(sleep_url)
327
347
  end
348
+
349
+ it "is given a chance to handle a connection timeout error" do
350
+ allow(TCPSocket).to receive(:open) { sleep 1 }
351
+ sleep_url = "#{dummy.endpoint}/sleep"
352
+ feature_instance = feature_class.new
353
+
354
+ expect do
355
+ client.use(:test_feature => feature_instance).
356
+ timeout(0.001).
357
+ request(:post, sleep_url)
358
+ end.to raise_error(HTTP::TimeoutError)
359
+ expect(feature_instance.captured_error).to be_a(HTTP::TimeoutError)
360
+ end
328
361
  end
329
362
  end
330
363
 
@@ -335,7 +368,8 @@ RSpec.describe HTTP::Client do
335
368
  let(:client) { described_class.new(options.merge(extra_options)) }
336
369
  end
337
370
 
338
- describe "working with SSL" do
371
+ # TODO: https://github.com/httprb/http/issues/627
372
+ xdescribe "working with SSL" do
339
373
  run_server(:dummy_ssl) { DummyServer.new(:ssl => true) }
340
374
 
341
375
  let(:extra_options) { {} }
@@ -476,7 +510,7 @@ RSpec.describe HTTP::Client do
476
510
  BODY
477
511
  end
478
512
 
479
- it "raises HTTP::ConnectionError" do
513
+ xit "raises HTTP::ConnectionError" do
480
514
  expect { client.get(dummy.endpoint).to_s }.to raise_error(HTTP::ConnectionError)
481
515
  end
482
516
  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(
@@ -39,19 +60,4 @@ RSpec.describe HTTP::Features::Instrumentation do
39
60
  expect(instrumenter.output[:finish]).to eq(:response => response)
40
61
  end
41
62
  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
63
  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
@@ -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
@@ -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)
@@ -62,7 +62,7 @@ RSpec.describe HTTP::Response::Parser do
62
62
  end
63
63
 
64
64
  context "when response is feeded in many parts" do
65
- let(:parts) { raw_response.split(//) }
65
+ let(:parts) { raw_response.chars }
66
66
 
67
67
  it "skips to next non-info response" do
68
68
  expect(subject.status_code).to eq(200)
@@ -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