http 4.2.0 → 5.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) 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/layout.yml +8 -0
  6. data/.rubocop/style.yml +32 -0
  7. data/.rubocop.yml +8 -110
  8. data/.rubocop_todo.yml +192 -0
  9. data/.yardopts +1 -1
  10. data/CHANGES.md +168 -0
  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 +5 -5
  16. data/lib/http/chainable.rb +23 -17
  17. data/lib/http/client.rb +52 -35
  18. data/lib/http/connection.rb +12 -8
  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 +7 -7
  22. data/lib/http/features/auto_inflate.rb +6 -7
  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 +6 -9
  29. data/lib/http/redirector.rb +4 -2
  30. data/lib/http/request/body.rb +1 -0
  31. data/lib/http/request/writer.rb +8 -3
  32. data/lib/http/request.rb +28 -11
  33. data/lib/http/response/body.rb +6 -4
  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/response.rb +35 -15
  38. data/lib/http/timeout/global.rb +42 -38
  39. data/lib/http/timeout/null.rb +2 -1
  40. data/lib/http/timeout/per_operation.rb +56 -58
  41. data/lib/http/uri.rb +5 -5
  42. data/lib/http/version.rb +1 -1
  43. data/spec/lib/http/client_spec.rb +173 -35
  44. data/spec/lib/http/connection_spec.rb +8 -5
  45. data/spec/lib/http/features/auto_inflate_spec.rb +3 -2
  46. data/spec/lib/http/features/instrumentation_spec.rb +27 -21
  47. data/spec/lib/http/features/logging_spec.rb +8 -10
  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 +59 -1
  52. data/spec/lib/http/request/writer_spec.rb +25 -2
  53. data/spec/lib/http/request_spec.rb +5 -5
  54. data/spec/lib/http/response/body_spec.rb +5 -5
  55. data/spec/lib/http/response/parser_spec.rb +74 -0
  56. data/spec/lib/http/response/status_spec.rb +3 -3
  57. data/spec/lib/http/response_spec.rb +44 -3
  58. data/spec/lib/http_spec.rb +30 -3
  59. data/spec/spec_helper.rb +21 -21
  60. data/spec/support/black_hole.rb +1 -1
  61. data/spec/support/dummy_server/servlet.rb +17 -6
  62. data/spec/support/dummy_server.rb +7 -7
  63. data/spec/support/fuubar.rb +21 -0
  64. data/spec/support/http_handling_shared.rb +4 -4
  65. data/spec/support/simplecov.rb +19 -0
  66. data/spec/support/ssl_helper.rb +4 -4
  67. metadata +24 -16
  68. data/.coveralls.yml +0 -1
  69. data/.travis.yml +0 -37
data/lib/http/response.rb CHANGED
@@ -7,7 +7,6 @@ require "http/content_type"
7
7
  require "http/mime_type"
8
8
  require "http/response/status"
9
9
  require "http/response/inflater"
10
- require "http/uri"
11
10
  require "http/cookie_jar"
12
11
  require "time"
13
12
 
@@ -26,8 +25,8 @@ module HTTP
26
25
  # @return [Body]
27
26
  attr_reader :body
28
27
 
29
- # @return [URI, nil]
30
- attr_reader :uri
28
+ # @return [Request]
29
+ attr_reader :request
31
30
 
32
31
  # @return [Hash]
33
32
  attr_reader :proxy_headers
@@ -41,10 +40,11 @@ module HTTP
41
40
  # @option opts [HTTP::Connection] :connection
42
41
  # @option opts [String] :encoding Encoding to use when reading body
43
42
  # @option opts [String] :body
44
- # @option opts [String] :uri
43
+ # @option opts [HTTP::Request] request The request this is in response to.
44
+ # @option opts [String] :uri (DEPRECATED) used to populate a missing request
45
45
  def initialize(opts)
46
46
  @version = opts.fetch(:version)
47
- @uri = HTTP::URI.parse(opts.fetch(:uri)) if opts.include? :uri
47
+ @request = init_request(opts)
48
48
  @status = HTTP::Response::Status.new(opts.fetch(:status))
49
49
  @headers = HTTP::Headers.coerce(opts[:headers] || {})
50
50
  @proxy_headers = HTTP::Headers.coerce(opts[:proxy_headers] || {})
@@ -61,24 +61,28 @@ module HTTP
61
61
 
62
62
  # @!method reason
63
63
  # @return (see HTTP::Response::Status#reason)
64
- def_delegator :status, :reason
64
+ def_delegator :@status, :reason
65
65
 
66
66
  # @!method code
67
67
  # @return (see HTTP::Response::Status#code)
68
- def_delegator :status, :code
68
+ def_delegator :@status, :code
69
69
 
70
70
  # @!method to_s
71
71
  # (see HTTP::Response::Body#to_s)
72
- def_delegator :body, :to_s
72
+ def_delegator :@body, :to_s
73
73
  alias to_str to_s
74
74
 
75
75
  # @!method readpartial
76
76
  # (see HTTP::Response::Body#readpartial)
77
- def_delegator :body, :readpartial
77
+ def_delegator :@body, :readpartial
78
78
 
79
79
  # @!method connection
80
80
  # (see HTTP::Response::Body#connection)
81
- def_delegator :body, :connection
81
+ def_delegator :@body, :connection
82
+
83
+ # @!method uri
84
+ # @return (see HTTP::Request#uri)
85
+ def_delegator :@request, :uri
82
86
 
83
87
  # Returns an Array ala Rack: `[status, headers, body]`
84
88
  #
@@ -150,17 +154,33 @@ module HTTP
150
154
 
151
155
  # Parse response body with corresponding MIME type adapter.
152
156
  #
153
- # @param [#to_s] as Parse as given MIME type
154
- # instead of the one determined from headers
155
- # @raise [HTTP::Error] if adapter not found
157
+ # @param type [#to_s] Parse as given MIME type.
158
+ # @raise (see MimeType.[])
156
159
  # @return [Object]
157
- def parse(as = nil)
158
- MimeType[as || mime_type].decode to_s
160
+ def parse(type = nil)
161
+ MimeType[type || mime_type].decode to_s
159
162
  end
160
163
 
161
164
  # Inspect a response
162
165
  def inspect
163
166
  "#<#{self.class}/#{@version} #{code} #{reason} #{headers.to_h.inspect}>"
164
167
  end
168
+
169
+ private
170
+
171
+ # Initialize an HTTP::Request from options.
172
+ #
173
+ # @return [HTTP::Request]
174
+ def init_request(opts)
175
+ raise ArgumentError, ":uri is for backwards compatibilty and conflicts with :request" \
176
+ if opts[:request] && opts[:uri]
177
+
178
+ # For backwards compatibilty
179
+ if opts[:uri]
180
+ HTTP::Request.new(:uri => opts[:uri], :verb => :get)
181
+ else
182
+ opts.fetch(:request)
183
+ end
184
+ end
165
185
  end
166
186
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "timeout"
4
3
  require "io/wait"
4
+ require "resolv"
5
+ require "timeout"
5
6
 
6
7
  require "http/timeout/null"
7
8
 
@@ -12,6 +13,9 @@ module HTTP
12
13
  super
13
14
 
14
15
  @timeout = @time_left = options.fetch(:global_timeout)
16
+ @dns_resolver = options.fetch(:dns_resolver) do
17
+ ::Resolv.method(:getaddresses)
18
+ end
15
19
  end
16
20
 
17
21
  # To future me: Don't remove this again, past you was smarter.
@@ -19,14 +23,28 @@ module HTTP
19
23
  @time_left = @timeout
20
24
  end
21
25
 
22
- def connect(socket_class, host, port, nodelay = false)
26
+ def connect(socket_class, host_name, *args)
27
+ connect_operation = lambda do |host_address|
28
+ ::Timeout.timeout(@time_left, TimeoutError) do
29
+ super(socket_class, host_address, *args)
30
+ end
31
+ end
32
+ host_addresses = @dns_resolver.call(host_name)
33
+ # ensure something to iterates
34
+ trying_targets = host_addresses.empty? ? [host_name] : host_addresses
23
35
  reset_timer
24
- ::Timeout.timeout(@time_left, TimeoutError) do
25
- @socket = socket_class.open(host, port)
26
- @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
36
+ trying_iterator = trying_targets.lazy
37
+ error = nil
38
+ begin
39
+ connect_operation.call(trying_iterator.next).tap do
40
+ log_time
41
+ end
42
+ rescue TimeoutError => e
43
+ error = e
44
+ retry
45
+ rescue ::StopIteration
46
+ raise error
27
47
  end
28
-
29
- log_time
30
48
  end
31
49
 
32
50
  def connect_ssl
@@ -59,22 +77,12 @@ module HTTP
59
77
 
60
78
  private
61
79
 
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
80
+ def read_nonblock(size, buffer = nil)
81
+ @socket.read_nonblock(size, buffer, :exception => false)
82
+ end
74
83
 
75
- def write_nonblock(data)
76
- @socket.write_nonblock(data, :exception => false)
77
- end
84
+ def write_nonblock(data)
85
+ @socket.write_nonblock(data, :exception => false)
78
86
  end
79
87
 
80
88
  # Perform the given I/O operation with the given argument
@@ -82,20 +90,18 @@ module HTTP
82
90
  reset_timer
83
91
 
84
92
  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
93
+ result = yield
94
+
95
+ case result
96
+ when :wait_readable then wait_readable_or_timeout
97
+ when :wait_writable then wait_writable_or_timeout
98
+ when NilClass then return :eof
99
+ else return result
98
100
  end
101
+ rescue IO::WaitReadable
102
+ wait_readable_or_timeout
103
+ rescue IO::WaitWritable
104
+ wait_writable_or_timeout
99
105
  end
100
106
  rescue EOFError
101
107
  :eof
@@ -121,9 +127,7 @@ module HTTP
121
127
 
122
128
  def log_time
123
129
  @time_left -= (Time.now - @started)
124
- if @time_left <= 0
125
- raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds"
126
- end
130
+ raise TimeoutError, "Timed out after using the allocated #{@timeout} seconds" if @time_left <= 0
127
131
 
128
132
  reset_timer
129
133
  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
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "resolv"
3
4
  require "timeout"
4
5
 
5
6
  require "http/timeout/null"
@@ -17,14 +18,34 @@ module HTTP
17
18
  @read_timeout = options.fetch(:read_timeout, READ_TIMEOUT)
18
19
  @write_timeout = options.fetch(:write_timeout, WRITE_TIMEOUT)
19
20
  @connect_timeout = options.fetch(:connect_timeout, CONNECT_TIMEOUT)
21
+ @dns_resolver = options.fetch(:dns_resolver) do
22
+ ::Resolv.method(:getaddresses)
23
+ end
20
24
  end
21
25
 
22
- def connect(socket_class, host, port, nodelay = false)
23
- ::Timeout.timeout(@connect_timeout, TimeoutError) do
24
- @socket = socket_class.open(host, port)
25
- @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
26
+ # TODO: refactor
27
+ # rubocop:disable Metrics/MethodLength
28
+ def connect(socket_class, host_name, *args)
29
+ connect_operation = lambda do |host_address|
30
+ ::Timeout.timeout(@connect_timeout, TimeoutError) do
31
+ super(socket_class, host_address, *args)
32
+ end
33
+ end
34
+ host_addresses = @dns_resolver.call(host_name)
35
+ # ensure something to iterates
36
+ trying_targets = host_addresses.empty? ? [host_name] : host_addresses
37
+ trying_iterator = trying_targets.lazy
38
+ error = nil
39
+ begin
40
+ connect_operation.call(trying_iterator.next)
41
+ rescue TimeoutError => e
42
+ error = e
43
+ retry
44
+ rescue ::StopIteration
45
+ raise error
26
46
  end
27
47
  end
48
+ # rubocop:enable Metrics/MethodLength
28
49
 
29
50
  def connect_ssl
30
51
  rescue_readable(@connect_timeout) do
@@ -34,65 +55,42 @@ module HTTP
34
55
  end
35
56
  end
36
57
 
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
58
+ # Read data from the socket
59
+ def readpartial(size, buffer = nil)
60
+ timeout = false
61
+ loop do
62
+ result = @socket.read_nonblock(size, buffer, :exception => false)
63
+
64
+ return :eof if result.nil?
65
+ return result if result != :wait_readable
66
+
67
+ raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
68
+
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)
81
80
  end
81
+ end
82
82
 
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
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
89
89
 
90
- raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
90
+ raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
91
91
 
92
- timeout = true unless @socket.to_io.wait_writable(@write_timeout)
93
- end
92
+ timeout = true unless @socket.to_io.wait_writable(@write_timeout)
94
93
  end
95
-
96
94
  end
97
95
  end
98
96
  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.2.0"
4
+ VERSION = "5.0.2"
5
5
  end