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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +65 -0
- data/.gitignore +6 -10
- data/.rspec +0 -4
- data/.rubocop/layout.yml +8 -0
- data/.rubocop/style.yml +32 -0
- data/.rubocop.yml +8 -110
- data/.rubocop_todo.yml +192 -0
- data/.yardopts +1 -1
- data/CHANGES.md +168 -0
- data/Gemfile +18 -10
- data/LICENSE.txt +1 -1
- data/README.md +17 -20
- data/Rakefile +2 -10
- data/http.gemspec +5 -5
- data/lib/http/chainable.rb +23 -17
- data/lib/http/client.rb +52 -35
- data/lib/http/connection.rb +12 -8
- data/lib/http/content_type.rb +12 -7
- data/lib/http/feature.rb +3 -1
- data/lib/http/features/auto_deflate.rb +7 -7
- data/lib/http/features/auto_inflate.rb +6 -7
- data/lib/http/features/instrumentation.rb +1 -1
- data/lib/http/features/logging.rb +19 -21
- data/lib/http/headers.rb +50 -13
- data/lib/http/mime_type/adapter.rb +3 -1
- data/lib/http/mime_type/json.rb +1 -0
- data/lib/http/options.rb +6 -9
- data/lib/http/redirector.rb +4 -2
- data/lib/http/request/body.rb +1 -0
- data/lib/http/request/writer.rb +8 -3
- data/lib/http/request.rb +28 -11
- data/lib/http/response/body.rb +6 -4
- data/lib/http/response/inflater.rb +1 -1
- data/lib/http/response/parser.rb +75 -49
- data/lib/http/response/status.rb +4 -3
- data/lib/http/response.rb +35 -15
- data/lib/http/timeout/global.rb +42 -38
- data/lib/http/timeout/null.rb +2 -1
- data/lib/http/timeout/per_operation.rb +56 -58
- data/lib/http/uri.rb +5 -5
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +173 -35
- data/spec/lib/http/connection_spec.rb +8 -5
- data/spec/lib/http/features/auto_inflate_spec.rb +3 -2
- data/spec/lib/http/features/instrumentation_spec.rb +27 -21
- data/spec/lib/http/features/logging_spec.rb +8 -10
- data/spec/lib/http/headers_spec.rb +53 -18
- data/spec/lib/http/options/headers_spec.rb +1 -1
- data/spec/lib/http/options/merge_spec.rb +16 -16
- data/spec/lib/http/redirector_spec.rb +59 -1
- data/spec/lib/http/request/writer_spec.rb +25 -2
- data/spec/lib/http/request_spec.rb +5 -5
- data/spec/lib/http/response/body_spec.rb +5 -5
- data/spec/lib/http/response/parser_spec.rb +74 -0
- data/spec/lib/http/response/status_spec.rb +3 -3
- data/spec/lib/http/response_spec.rb +44 -3
- data/spec/lib/http_spec.rb +30 -3
- data/spec/spec_helper.rb +21 -21
- data/spec/support/black_hole.rb +1 -1
- data/spec/support/dummy_server/servlet.rb +17 -6
- data/spec/support/dummy_server.rb +7 -7
- data/spec/support/fuubar.rb +21 -0
- data/spec/support/http_handling_shared.rb +4 -4
- data/spec/support/simplecov.rb +19 -0
- data/spec/support/ssl_helper.rb +4 -4
- metadata +24 -16
- data/.coveralls.yml +0 -1
- 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 [
|
30
|
-
attr_reader :
|
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 [
|
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
|
-
@
|
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
|
64
|
+
def_delegator :@status, :reason
|
65
65
|
|
66
66
|
# @!method code
|
67
67
|
# @return (see HTTP::Response::Status#code)
|
68
|
-
def_delegator
|
68
|
+
def_delegator :@status, :code
|
69
69
|
|
70
70
|
# @!method to_s
|
71
71
|
# (see HTTP::Response::Body#to_s)
|
72
|
-
def_delegator
|
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
|
77
|
+
def_delegator :@body, :readpartial
|
78
78
|
|
79
79
|
# @!method connection
|
80
80
|
# (see HTTP::Response::Body#connection)
|
81
|
-
def_delegator
|
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]
|
154
|
-
#
|
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(
|
158
|
-
MimeType[
|
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
|
data/lib/http/timeout/global.rb
CHANGED
@@ -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,
|
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
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
76
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
data/lib/http/timeout/null.rb
CHANGED
@@ -12,7 +12,7 @@ module HTTP
|
|
12
12
|
|
13
13
|
attr_reader :options, :socket
|
14
14
|
|
15
|
-
def initialize(options = {})
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
:eof
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
90
|
+
raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout
|
91
91
|
|
92
|
-
|
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
|
35
|
-
:authority
|
36
|
-
:path
|
37
|
-
:query
|
38
|
-
: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