http 4.2.0 → 5.0.2

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