http 4.4.1 → 5.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) 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 +112 -3
  11. data/Gemfile +18 -10
  12. data/LICENSE.txt +1 -1
  13. data/README.md +17 -20
  14. data/Rakefile +2 -10
  15. data/http.gemspec +3 -3
  16. data/lib/http/chainable.rb +23 -17
  17. data/lib/http/client.rb +36 -30
  18. data/lib/http/connection.rb +11 -7
  19. data/lib/http/content_type.rb +12 -7
  20. data/lib/http/feature.rb +3 -1
  21. data/lib/http/features/auto_deflate.rb +6 -6
  22. data/lib/http/features/auto_inflate.rb +6 -5
  23. data/lib/http/features/instrumentation.rb +1 -1
  24. data/lib/http/features/logging.rb +19 -21
  25. data/lib/http/headers.rb +50 -13
  26. data/lib/http/mime_type/adapter.rb +3 -1
  27. data/lib/http/mime_type/json.rb +1 -0
  28. data/lib/http/options.rb +5 -8
  29. data/lib/http/redirector.rb +2 -1
  30. data/lib/http/request.rb +28 -11
  31. data/lib/http/request/body.rb +1 -0
  32. data/lib/http/request/writer.rb +3 -2
  33. data/lib/http/response.rb +17 -15
  34. data/lib/http/response/body.rb +2 -2
  35. data/lib/http/response/inflater.rb +1 -1
  36. data/lib/http/response/parser.rb +74 -62
  37. data/lib/http/response/status.rb +4 -3
  38. data/lib/http/timeout/global.rb +17 -31
  39. data/lib/http/timeout/null.rb +2 -1
  40. data/lib/http/timeout/per_operation.rb +31 -54
  41. data/lib/http/uri.rb +5 -5
  42. data/lib/http/version.rb +1 -1
  43. data/spec/lib/http/client_spec.rb +119 -30
  44. data/spec/lib/http/connection_spec.rb +8 -5
  45. data/spec/lib/http/features/auto_inflate_spec.rb +4 -2
  46. data/spec/lib/http/features/instrumentation_spec.rb +28 -21
  47. data/spec/lib/http/features/logging_spec.rb +8 -9
  48. data/spec/lib/http/headers_spec.rb +53 -18
  49. data/spec/lib/http/options/headers_spec.rb +1 -1
  50. data/spec/lib/http/options/merge_spec.rb +16 -16
  51. data/spec/lib/http/redirector_spec.rb +46 -1
  52. data/spec/lib/http/request/writer_spec.rb +13 -1
  53. data/spec/lib/http/request_spec.rb +5 -5
  54. data/spec/lib/http/response/parser_spec.rb +33 -4
  55. data/spec/lib/http/response/status_spec.rb +3 -3
  56. data/spec/lib/http/response_spec.rb +5 -3
  57. data/spec/lib/http_spec.rb +30 -3
  58. data/spec/spec_helper.rb +21 -21
  59. data/spec/support/black_hole.rb +1 -1
  60. data/spec/support/dummy_server.rb +7 -7
  61. data/spec/support/dummy_server/servlet.rb +17 -6
  62. data/spec/support/fuubar.rb +21 -0
  63. data/spec/support/http_handling_shared.rb +4 -4
  64. data/spec/support/simplecov.rb +19 -0
  65. data/spec/support/ssl_helper.rb +4 -4
  66. metadata +18 -12
  67. data/.coveralls.yml +0 -1
  68. data/.travis.yml +0 -39
@@ -58,7 +58,7 @@ module HTTP
58
58
  # SYMBOLS[418] # => :im_a_teapot
59
59
  #
60
60
  # @return [Hash<Fixnum => Symbol>]
61
- SYMBOLS = Hash[REASONS.map { |k, v| [k, symbolize(v)] }].freeze
61
+ SYMBOLS = REASONS.transform_values { |v| symbolize(v) }.freeze
62
62
 
63
63
  # Reversed {SYMBOLS} map.
64
64
  #
@@ -69,7 +69,7 @@ module HTTP
69
69
  # SYMBOL_CODES[:im_a_teapot] # => 418
70
70
  #
71
71
  # @return [Hash<Symbol => Fixnum>]
72
- SYMBOL_CODES = Hash[SYMBOLS.map { |k, v| [v, k] }].freeze
72
+ SYMBOL_CODES = SYMBOLS.map { |k, v| [v, k] }.to_h.freeze
73
73
 
74
74
  # @return [Fixnum] status code
75
75
  attr_reader :code
@@ -132,7 +132,7 @@ module HTTP
132
132
  end
133
133
 
134
134
  SYMBOLS.each do |code, symbol|
135
- class_eval <<-RUBY, __FILE__, __LINE__
135
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
136
136
  def #{symbol}? # def bad_request?
137
137
  #{code} == code # 400 == code
138
138
  end # end
@@ -141,6 +141,7 @@ module HTTP
141
141
 
142
142
  def __setobj__(obj)
143
143
  raise TypeError, "Expected #{obj.inspect} to respond to #to_i" unless obj.respond_to? :to_i
144
+
144
145
  @code = obj.to_i
145
146
  end
146
147
 
@@ -59,22 +59,12 @@ module HTTP
59
59
 
60
60
  private
61
61
 
62
- if RUBY_VERSION < "2.1.0"
63
- def read_nonblock(size, buffer = nil)
64
- @socket.read_nonblock(size, buffer)
65
- end
66
-
67
- def write_nonblock(data)
68
- @socket.write_nonblock(data)
69
- end
70
- else
71
- def read_nonblock(size, buffer = nil)
72
- @socket.read_nonblock(size, buffer, :exception => false)
73
- end
62
+ def read_nonblock(size, buffer = nil)
63
+ @socket.read_nonblock(size, buffer, :exception => false)
64
+ end
74
65
 
75
- def write_nonblock(data)
76
- @socket.write_nonblock(data, :exception => false)
77
- end
66
+ def write_nonblock(data)
67
+ @socket.write_nonblock(data, :exception => false)
78
68
  end
79
69
 
80
70
  # Perform the given I/O operation with the given argument
@@ -82,20 +72,18 @@ module HTTP
82
72
  reset_timer
83
73
 
84
74
  loop do
85
- begin
86
- result = yield
87
-
88
- case result
89
- when :wait_readable then wait_readable_or_timeout
90
- when :wait_writable then wait_writable_or_timeout
91
- when NilClass then return :eof
92
- else return result
93
- end
94
- rescue IO::WaitReadable
95
- wait_readable_or_timeout
96
- rescue IO::WaitWritable
97
- wait_writable_or_timeout
75
+ result = yield
76
+
77
+ case result
78
+ when :wait_readable then wait_readable_or_timeout
79
+ when :wait_writable then wait_writable_or_timeout
80
+ when NilClass then return :eof
81
+ else return result
98
82
  end
83
+ rescue IO::WaitReadable
84
+ wait_readable_or_timeout
85
+ rescue IO::WaitWritable
86
+ wait_writable_or_timeout
99
87
  end
100
88
  rescue EOFError
101
89
  :eof
@@ -121,9 +109,7 @@ module HTTP
121
109
 
122
110
  def log_time
123
111
  @time_left -= (Time.now - @started)
124
- if @time_left <= 0
125
- raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds"
126
- end
112
+ raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds" if @time_left <= 0
127
113
 
128
114
  reset_timer
129
115
  end
@@ -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.1"
4
+ VERSION = "5.0.1"
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