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
@@ -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,65 +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
- # marking the socket for timeout. Why is this not being raised immediately?
70
- # it seems there is some race-condition on the network level between calling
71
- # #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
72
- # for reads, and when waiting for x seconds, it returns nil suddenly without completing
73
- # the x seconds. In a normal case this would be a timeout on wait/read, but it can
74
- # also mean that the socket has been closed by the server. Therefore we "mark" the
75
- # socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
76
- # timeout. Else, the first timeout was a proper timeout.
77
- # This hack has to be done because io/wait#wait_readable doesn't provide a value for when
78
- # the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
79
- timeout = true unless @socket.to_io.wait_readable(@read_timeout)
80
- 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)
81
59
  end
60
+ end
82
61
 
83
- # Write data to the socket
84
- def write(data)
85
- timeout = false
86
- loop do
87
- result = @socket.write_nonblock(data, :exception => false)
88
- 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
89
68
 
90
- raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
69
+ raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
91
70
 
92
- timeout = true unless @socket.to_io.wait_writable(@write_timeout)
93
- end
71
+ timeout = true unless @socket.to_io.wait_writable(@write_timeout)
94
72
  end
95
-
96
73
  end
97
74
  end
98
75
  end
data/lib/http/uri.rb CHANGED
@@ -31,11 +31,11 @@ module HTTP
31
31
  uri = HTTP::URI.parse uri
32
32
 
33
33
  HTTP::URI.new(
34
- :scheme => uri.normalized_scheme,
35
- :authority => uri.normalized_authority,
36
- :path => uri.normalized_path,
37
- :query => uri.query,
38
- :fragment => uri.normalized_fragment
34
+ :scheme => uri.normalized_scheme,
35
+ :authority => uri.normalized_authority,
36
+ :path => uri.normalized_path,
37
+ :query => uri.query,
38
+ :fragment => uri.normalized_fragment
39
39
  )
40
40
  end
41
41
 
data/lib/http/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HTTP
4
- VERSION = "4.4.0"
4
+ VERSION = "5.0.0"
5
5
  end
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
1
  # coding: utf-8
2
+ # frozen_string_literal: true
3
3
 
4
4
  require "support/http_handling_shared"
5
5
  require "support/dummy_server"
@@ -8,39 +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
- stubs.fetch(request.uri) { super(request, options) }
14
- 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
15
17
 
16
- def stubs
17
- @stubs ||= {}
18
- end
18
+ def stubs
19
+ @stubs ||= {}
20
+ end
19
21
 
20
- def stub(stubs)
21
- @stubs = stubs.each_with_object({}) do |(k, v), o|
22
- o[HTTP::URI.parse k] = v
22
+ def stub(stubs)
23
+ @stubs = stubs.transform_keys do |k|
24
+ HTTP::URI::NORMALIZER.call(k).to_s
25
+ end
26
+
27
+ self
23
28
  end
29
+ end
24
30
 
25
- self
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
26
41
  end
27
- end
28
42
 
29
- def redirect_response(location, status = 302)
30
- HTTP::Response.new(
31
- :status => status,
32
- :version => "1.1",
33
- :headers => {"Location" => location},
34
- :body => ""
35
- )
36
- end
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
52
+ end
37
53
 
38
- def simple_response(body, status = 200)
39
- HTTP::Response.new(
40
- :status => status,
41
- :version => "1.1",
42
- :body => body
43
- )
54
+ stub_const("StubbedClient", stubbed_client)
44
55
  end
45
56
 
46
57
  describe "following redirects" do
@@ -193,7 +204,7 @@ RSpec.describe HTTP::Client do
193
204
  context "when passing an HTTP::FormData object directly" do
194
205
  it "creates url encoded form data object" do
195
206
  client = HTTP::Client.new
196
- form_data = HTTP::FormData::Multipart.new(:foo => "bar")
207
+ form_data = HTTP::FormData::Multipart.new({ :foo => "bar" })
197
208
 
198
209
  allow(client).to receive(:perform)
199
210
 
@@ -279,6 +290,75 @@ RSpec.describe HTTP::Client do
279
290
  end
280
291
  end
281
292
  end
293
+
294
+ context "Feature" do
295
+ let(:feature_class) do
296
+ Class.new(HTTP::Feature) do
297
+ attr_reader :captured_request, :captured_response, :captured_error
298
+
299
+ def wrap_request(request)
300
+ @captured_request = request
301
+ end
302
+
303
+ def wrap_response(response)
304
+ @captured_response = response
305
+ end
306
+
307
+ def on_error(request, error)
308
+ @captured_request = request
309
+ @captured_error = error
310
+ end
311
+ end
312
+ end
313
+
314
+ it "is given a chance to wrap the Request" do
315
+ feature_instance = feature_class.new
316
+
317
+ response = client.use(:test_feature => feature_instance).
318
+ request(:get, dummy.endpoint)
319
+
320
+ expect(response.code).to eq(200)
321
+ expect(feature_instance.captured_request.verb).to eq(:get)
322
+ expect(feature_instance.captured_request.uri.to_s).to eq("#{dummy.endpoint}/")
323
+ end
324
+
325
+ it "is given a chance to wrap the Response" do
326
+ feature_instance = feature_class.new
327
+
328
+ response = client.use(:test_feature => feature_instance).
329
+ request(:get, dummy.endpoint)
330
+
331
+ expect(feature_instance.captured_response).to eq(response)
332
+ end
333
+
334
+ it "is given a chance to handle an error" do
335
+ sleep_url = "#{dummy.endpoint}/sleep"
336
+ feature_instance = feature_class.new
337
+
338
+ expect do
339
+ client.use(:test_feature => feature_instance).
340
+ timeout(0.2).
341
+ request(:post, sleep_url)
342
+ end.to raise_error(HTTP::TimeoutError)
343
+
344
+ expect(feature_instance.captured_error).to be_a(HTTP::TimeoutError)
345
+ expect(feature_instance.captured_request.verb).to eq(:post)
346
+ expect(feature_instance.captured_request.uri.to_s).to eq(sleep_url)
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
361
+ end
282
362
  end
283
363
 
284
364
  include_context "HTTP handling" do
@@ -288,7 +368,8 @@ RSpec.describe HTTP::Client do
288
368
  let(:client) { described_class.new(options.merge(extra_options)) }
289
369
  end
290
370
 
291
- describe "working with SSL" do
371
+ # TODO: https://github.com/httprb/http/issues/627
372
+ xdescribe "working with SSL" do
292
373
  run_server(:dummy_ssl) { DummyServer.new(:ssl => true) }
293
374
 
294
375
  let(:extra_options) { {} }
@@ -331,6 +412,14 @@ RSpec.describe HTTP::Client do
331
412
  client.get(dummy.endpoint).to_s
332
413
  end
333
414
 
415
+ it "provides access to the Request from the Response" do
416
+ unique_value = "20190424"
417
+ response = client.headers("X-Value" => unique_value).get(dummy.endpoint)
418
+
419
+ expect(response.request).to be_a(HTTP::Request)
420
+ expect(response.request.headers["X-Value"]).to eq(unique_value)
421
+ end
422
+
334
423
  context "with HEAD request" do
335
424
  it "does not iterates through body" do
336
425
  expect_any_instance_of(HTTP::Connection).to_not receive(:readpartial)
@@ -421,7 +510,7 @@ RSpec.describe HTTP::Client do
421
510
  BODY
422
511
  end
423
512
 
424
- it "raises HTTP::ConnectionError" do
513
+ xit "raises HTTP::ConnectionError" do
425
514
  expect { client.get(dummy.endpoint).to_s }.to raise_error(HTTP::ConnectionError)
426
515
  end
427
516
  end
@@ -3,9 +3,9 @@
3
3
  RSpec.describe HTTP::Connection do
4
4
  let(:req) do
5
5
  HTTP::Request.new(
6
- :verb => :get,
7
- :uri => "http://example.com/",
8
- :headers => {}
6
+ :verb => :get,
7
+ :uri => "http://example.com/",
8
+ :headers => {}
9
9
  )
10
10
  end
11
11
  let(:socket) { double(:connect => nil) }
@@ -20,14 +20,17 @@ RSpec.describe HTTP::Connection do
20
20
  <<-RESPONSE.gsub(/^\s*\| */, "").gsub(/\n/, "\r\n")
21
21
  | HTTP/1.1 200 OK
22
22
  | Content-Type: text
23
+ | foo_bar: 123
23
24
  |
24
25
  RESPONSE
25
26
  end
26
27
  end
27
28
 
28
- it "reads data in parts" do
29
+ it "populates headers collection, preserving casing" do
29
30
  connection.read_headers!
30
- expect(connection.headers).to eq("Content-Type" => "text")
31
+ expect(connection.headers).to eq("Content-Type" => "text", "foo_bar" => "123")
32
+ expect(connection.headers["Foo-Bar"]).to eq("123")
33
+ expect(connection.headers["foo_bar"]).to eq("123")
31
34
  end
32
35
  end
33
36
 
@@ -11,7 +11,8 @@ RSpec.describe HTTP::Features::AutoInflate do
11
11
  :version => "1.1",
12
12
  :status => 200,
13
13
  :headers => headers,
14
- :connection => connection
14
+ :connection => connection,
15
+ :request => HTTP::Request.new(:verb => :get, :uri => "http://example.com")
15
16
  )
16
17
  end
17
18
 
@@ -73,7 +74,8 @@ RSpec.describe HTTP::Features::AutoInflate do
73
74
  :status => 200,
74
75
  :headers => {:content_encoding => "gzip"},
75
76
  :connection => connection,
76
- :uri => "https://example.com"
77
+ :uri => "https://example.com",
78
+ :request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
77
79
  )
78
80
  end
79
81
 
@@ -2,15 +2,36 @@
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(
10
- :verb => :post,
11
- :uri => "https://example.com/",
31
+ :verb => :post,
32
+ :uri => "https://example.com/",
12
33
  :headers => {:accept => "application/json"},
13
- :body => '{"hello": "world!"}'
34
+ :body => '{"hello": "world!"}'
14
35
  )
15
36
  end
16
37
 
@@ -25,10 +46,11 @@ RSpec.describe HTTP::Features::Instrumentation do
25
46
  let(:response) do
26
47
  HTTP::Response.new(
27
48
  :version => "1.1",
28
- :uri => "https://example.com",
29
- :status => 200,
49
+ :uri => "https://example.com",
50
+ :status => 200,
30
51
  :headers => {:content_type => "application/json"},
31
- :body => '{"success": true}'
52
+ :body => '{"success": true}',
53
+ :request => HTTP::Request.new(:verb => :get, :uri => "https://example.com")
32
54
  )
33
55
  end
34
56
 
@@ -38,19 +60,4 @@ RSpec.describe HTTP::Features::Instrumentation do
38
60
  expect(instrumenter.output[:finish]).to eq(:response => response)
39
61
  end
40
62
  end
41
-
42
- class TestInstrumenter < HTTP::Features::Instrumentation::NullInstrumenter
43
- attr_reader :output
44
- def initialize
45
- @output = {}
46
- end
47
-
48
- def start(_name, payload)
49
- output[:start] = payload
50
- end
51
-
52
- def finish(_name, payload)
53
- output[:finish] = payload
54
- end
55
- end
56
63
  end