http 5.0.0.pre3 → 5.0.0

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 (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