http 0.8.0.pre3 → 0.8.0.pre4
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/CHANGES.md +1 -1
- data/lib/http.rb +3 -0
- data/lib/http/client.rb +1 -0
- data/lib/http/connection.rb +11 -21
- data/lib/http/errors.rb +3 -0
- data/lib/http/options.rb +6 -2
- data/lib/http/timeout/global.rb +99 -0
- data/lib/http/timeout/null.rb +51 -0
- data/lib/http/timeout/per_operation.rb +117 -0
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +20 -30
- data/spec/lib/http/options/merge_spec.rb +3 -0
- data/spec/support/dummy_server/servlet.rb +14 -0
- data/spec/support/http_handling_shared.rb +207 -0
- metadata +7 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b7920fa65897a4adb435be3f24272554a59bb8f0
|
4
|
+
data.tar.gz: 9602b19d0e5f70eea417e69343a81159c356c3be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e295f5c7d2b7c34ff6c4e0da1746361b777903d354910f0003eaded8c14cc180cd7e6e1edb11ce6980c63b851d0290c6e2dc0ba44c7bdee3cce036ee2ce17f3f
|
7
|
+
data.tar.gz: 7956cd926d6060cdc7186e1546bf8b33192f82cb71ce288c98b19f467b9e8192a4d3cfa92fd2a789e9c2dcf5631a89c7bda9136f359b8f1bb0e15a16fc14a9f0
|
data/CHANGES.md
CHANGED
data/lib/http.rb
CHANGED
data/lib/http/client.rb
CHANGED
data/lib/http/connection.rb
CHANGED
@@ -16,14 +16,22 @@ module HTTP
|
|
16
16
|
|
17
17
|
@parser = Response::Parser.new
|
18
18
|
|
19
|
-
@socket = options[:
|
19
|
+
@socket = options[:timeout_class].new(options[:timeout_options])
|
20
|
+
@socket.connect(options[:socket_class], req.socket_host, req.socket_port)
|
20
21
|
|
21
|
-
start_tls(
|
22
|
+
@socket.start_tls(
|
23
|
+
req.uri.host,
|
24
|
+
options[:ssl_socket_class],
|
25
|
+
options[:ssl_context]
|
26
|
+
) if req.uri.is_a?(URI::HTTPS) && !req.using_proxy?
|
22
27
|
|
23
28
|
reset_timer
|
24
29
|
end
|
25
30
|
|
26
31
|
# Send a request to the server
|
32
|
+
#
|
33
|
+
# @param [Request] Request to send to the server
|
34
|
+
# @return [Nil]
|
27
35
|
def send_request(req)
|
28
36
|
if pending_response
|
29
37
|
fail StateError, "Tried to send a request while one is pending already. Make sure you read off the body."
|
@@ -60,7 +68,7 @@ module HTTP
|
|
60
68
|
chunk.to_s
|
61
69
|
end
|
62
70
|
|
63
|
-
# Reads data from socket up until headers
|
71
|
+
# Reads data from socket up until headers are loaded
|
64
72
|
def read_headers!
|
65
73
|
read_more BUFFER_SIZE until parser.headers
|
66
74
|
set_keep_alive
|
@@ -131,23 +139,5 @@ module HTTP
|
|
131
139
|
end
|
132
140
|
|
133
141
|
private :read_more
|
134
|
-
|
135
|
-
# Starts the SSL connection
|
136
|
-
def start_tls(host, ssl_socket_class, ssl_context)
|
137
|
-
# TODO: abstract away SSLContexts so we can use other TLS libraries
|
138
|
-
ssl_context ||= OpenSSL::SSL::SSLContext.new
|
139
|
-
@socket = ssl_socket_class.new(socket, ssl_context)
|
140
|
-
socket.sync_close = true
|
141
|
-
|
142
|
-
socket.connect
|
143
|
-
|
144
|
-
if ssl_context.verify_mode == OpenSSL::SSL::VERIFY_PEER
|
145
|
-
socket.post_connection_check(host)
|
146
|
-
end
|
147
|
-
|
148
|
-
socket
|
149
|
-
end
|
150
|
-
|
151
|
-
private :start_tls
|
152
142
|
end
|
153
143
|
end
|
data/lib/http/errors.rb
CHANGED
data/lib/http/options.rb
CHANGED
@@ -8,11 +8,13 @@ module HTTP
|
|
8
8
|
@default_socket_class = TCPSocket
|
9
9
|
@default_ssl_socket_class = OpenSSL::SSL::SSLSocket
|
10
10
|
|
11
|
+
@default_timeout_class = HTTP::Timeout::Null
|
12
|
+
|
11
13
|
@default_cache = Http::Cache::NullCache.new
|
12
14
|
|
13
15
|
class << self
|
14
16
|
attr_accessor :default_socket_class, :default_ssl_socket_class
|
15
|
-
attr_accessor :default_cache
|
17
|
+
attr_accessor :default_cache, :default_timeout_class
|
16
18
|
|
17
19
|
def new(options = {})
|
18
20
|
return options if options.is_a?(self)
|
@@ -41,6 +43,8 @@ module HTTP
|
|
41
43
|
def initialize(options = {})
|
42
44
|
defaults = {:response => :auto,
|
43
45
|
:proxy => {},
|
46
|
+
:timeout_class => self.class.default_timeout_class,
|
47
|
+
:timeout_options => {},
|
44
48
|
:socket_class => self.class.default_socket_class,
|
45
49
|
:ssl_socket_class => self.class.default_ssl_socket_class,
|
46
50
|
:cache => self.class.default_cache,
|
@@ -62,7 +66,7 @@ module HTTP
|
|
62
66
|
%w(
|
63
67
|
proxy params form json body follow response
|
64
68
|
socket_class ssl_socket_class ssl_context
|
65
|
-
persistent keep_alive_timeout
|
69
|
+
persistent keep_alive_timeout timeout_class timeout_options
|
66
70
|
).each do |method_name|
|
67
71
|
def_option method_name
|
68
72
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# rubocop:disable Lint/HandleExceptions
|
2
|
+
module HTTP
|
3
|
+
module Timeout
|
4
|
+
class Global < PerOperation
|
5
|
+
attr_reader :time_left, :total_timeout
|
6
|
+
|
7
|
+
def initialize(*args)
|
8
|
+
super
|
9
|
+
|
10
|
+
@time_left = connect_timeout + read_timeout + write_timeout
|
11
|
+
@total_timeout = time_left
|
12
|
+
end
|
13
|
+
|
14
|
+
# Abstracted out from the normal connect for SSL connections
|
15
|
+
def connect_with_timeout(*args)
|
16
|
+
reset_timer
|
17
|
+
|
18
|
+
begin
|
19
|
+
socket.connect_nonblock(*args)
|
20
|
+
|
21
|
+
rescue IO::WaitReadable
|
22
|
+
IO.select([socket], nil, nil, time_left)
|
23
|
+
log_time
|
24
|
+
retry
|
25
|
+
|
26
|
+
rescue Errno::EINPROGRESS
|
27
|
+
IO.select(nil, [socket], nil, time_left)
|
28
|
+
log_time
|
29
|
+
retry
|
30
|
+
|
31
|
+
rescue Errno::EISCONN
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Read from the socket
|
36
|
+
def readpartial(size)
|
37
|
+
reset_timer
|
38
|
+
|
39
|
+
begin
|
40
|
+
socket.read_nonblock(size)
|
41
|
+
rescue IO::WaitReadable
|
42
|
+
IO.select([socket], nil, nil, time_left)
|
43
|
+
log_time
|
44
|
+
retry
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Write to the socket
|
49
|
+
def write(data)
|
50
|
+
reset_timer
|
51
|
+
|
52
|
+
begin
|
53
|
+
socket << data
|
54
|
+
rescue IO::WaitWritable
|
55
|
+
IO.select(nil, [socket], nil, time_left)
|
56
|
+
log_time
|
57
|
+
retry
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Create a DNS resolver
|
64
|
+
def resolve_address(host)
|
65
|
+
addr = HostResolver.getaddress(host)
|
66
|
+
return addr if addr
|
67
|
+
|
68
|
+
reset_timer
|
69
|
+
|
70
|
+
addr = Resolv::DNS.open(:timeout => time_left) do |dns|
|
71
|
+
dns.getaddress
|
72
|
+
end
|
73
|
+
|
74
|
+
log_time
|
75
|
+
|
76
|
+
addr
|
77
|
+
|
78
|
+
rescue Resolv::ResolvTimeout
|
79
|
+
raise TimeoutError, "DNS timed out after #{total_timeout} seconds"
|
80
|
+
end
|
81
|
+
|
82
|
+
# Due to the run/retry nature of nonblocking I/O, it's easier to keep track of time
|
83
|
+
# via method calls instead of a block to monitor.
|
84
|
+
def reset_timer
|
85
|
+
@started = Time.now
|
86
|
+
end
|
87
|
+
|
88
|
+
def log_time
|
89
|
+
@time_left -= (Time.now - @started)
|
90
|
+
if time_left <= 0
|
91
|
+
fail TimeoutError, "Timed out after using the allocated #{total_timeout} seconds"
|
92
|
+
end
|
93
|
+
|
94
|
+
reset_timer
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
# rubocop:enable Lint/HandleExceptions
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
module Timeout
|
5
|
+
class Null
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@socket, :close, :closed?
|
9
|
+
|
10
|
+
attr_reader :options, :socket
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
# Connects to a socket
|
17
|
+
def connect(socket_class, host, port)
|
18
|
+
@socket = socket_class.open(host, port)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Starts a SSL connection on a socket
|
22
|
+
def connect_ssl
|
23
|
+
socket.connect
|
24
|
+
end
|
25
|
+
|
26
|
+
# Configures the SSL connection and starts the connection
|
27
|
+
def start_tls(host, ssl_socket_class, ssl_context)
|
28
|
+
# TODO: abstract away SSLContexts so we can use other TLS libraries
|
29
|
+
ssl_context ||= OpenSSL::SSL::SSLContext.new
|
30
|
+
@socket = ssl_socket_class.new(socket, ssl_context)
|
31
|
+
socket.sync_close = true
|
32
|
+
|
33
|
+
connect_ssl
|
34
|
+
|
35
|
+
socket.post_connection_check(host) if ssl_context.verify_mode == OpenSSL::SSL::VERIFY_PEER
|
36
|
+
end
|
37
|
+
|
38
|
+
# Read from the socket
|
39
|
+
def readpartial(size)
|
40
|
+
socket.readpartial(size)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Write to the socket
|
44
|
+
def write(data)
|
45
|
+
socket << data
|
46
|
+
end
|
47
|
+
|
48
|
+
alias_method :<<, :write
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# rubocop:disable Lint/HandleExceptions
|
2
|
+
require "resolv"
|
3
|
+
|
4
|
+
module HTTP
|
5
|
+
module Timeout
|
6
|
+
class PerOperation < Null
|
7
|
+
HostResolver = Resolv::Hosts.new.tap(&:lazy_initialize)
|
8
|
+
|
9
|
+
CONNECT_TIMEOUT = 0.25
|
10
|
+
WRITE_TIMEOUT = 0.25
|
11
|
+
READ_TIMEOUT = 0.25
|
12
|
+
|
13
|
+
attr_reader :read_timeout, :write_timeout, :connect_timeout
|
14
|
+
|
15
|
+
def initialize(*args)
|
16
|
+
super
|
17
|
+
|
18
|
+
@read_timeout = options.fetch(:read_timeout, READ_TIMEOUT)
|
19
|
+
@write_timeout = options.fetch(:write_timeout, WRITE_TIMEOUT)
|
20
|
+
@connect_timeout = options.fetch(:connect_timeout, CONNECT_TIMEOUT)
|
21
|
+
end
|
22
|
+
|
23
|
+
def connect(_, host, port)
|
24
|
+
# https://github.com/celluloid/celluloid-io/blob/master/lib/celluloid/io/tcp_socket.rb
|
25
|
+
begin
|
26
|
+
addr = Resolv::IPv4.create(host)
|
27
|
+
rescue ArgumentError
|
28
|
+
end
|
29
|
+
|
30
|
+
# Guess it's not IPv4! Is it IPv6?
|
31
|
+
begin
|
32
|
+
addr ||= Resolv::IPv6.create(host)
|
33
|
+
rescue ArgumentError
|
34
|
+
end
|
35
|
+
|
36
|
+
unless addr
|
37
|
+
addr = resolve_address(host)
|
38
|
+
fail Resolv::ResolvError, "DNS result has no information for #{host}" unless addr
|
39
|
+
end
|
40
|
+
|
41
|
+
case addr
|
42
|
+
when Resolv::IPv4
|
43
|
+
family = Socket::AF_INET
|
44
|
+
when Resolv::IPv6
|
45
|
+
family = Socket::AF_INET6
|
46
|
+
else fail ArgumentError, "unsupported address class: #{addr.class}"
|
47
|
+
end
|
48
|
+
|
49
|
+
@socket = Socket.new(family, Socket::SOCK_STREAM, 0)
|
50
|
+
|
51
|
+
connect_with_timeout(Socket.sockaddr_in(port, addr.to_s))
|
52
|
+
end
|
53
|
+
|
54
|
+
# No changes need to be made for the SSL connection
|
55
|
+
alias_method :connect_with_timeout, :connect_ssl
|
56
|
+
|
57
|
+
# Read data from the socket
|
58
|
+
def readpartial(size)
|
59
|
+
socket.read_nonblock(size)
|
60
|
+
rescue IO::WaitReadable
|
61
|
+
if IO.select([socket], nil, nil, read_timeout)
|
62
|
+
retry
|
63
|
+
else
|
64
|
+
raise TimeoutError, "Read timed out after #{read_timeout} seconds"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Write data to the socket
|
69
|
+
def write(data)
|
70
|
+
socket.write_nonblock(data)
|
71
|
+
rescue IO::WaitWritable
|
72
|
+
if IO.select(nil, [socket], nil, write_timeout)
|
73
|
+
retry
|
74
|
+
else
|
75
|
+
raise TimeoutError, "Read timed out after #{write_timeout} seconds"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# Actually do the connect after we're setup
|
82
|
+
def connect_with_timeout(*args)
|
83
|
+
socket.connect_nonblock(*args)
|
84
|
+
|
85
|
+
rescue IO::WaitReadable
|
86
|
+
if IO.select([socket], nil, nil, connect_timeout)
|
87
|
+
retry
|
88
|
+
else
|
89
|
+
raise TimeoutError, "Connection timed out after #{connect_timeout} seconds"
|
90
|
+
end
|
91
|
+
|
92
|
+
rescue Errno::EINPROGRESS
|
93
|
+
if IO.select(nil, [socket], nil, connect_timeout)
|
94
|
+
retry
|
95
|
+
else
|
96
|
+
raise TimeoutError, "Connection timed out after #{connect_timeout} seconds"
|
97
|
+
end
|
98
|
+
|
99
|
+
rescue Errno::EISCONN
|
100
|
+
end
|
101
|
+
|
102
|
+
# Create a DNS resolver
|
103
|
+
def resolve_address(host)
|
104
|
+
addr = HostResolver.getaddress(host)
|
105
|
+
return addr if addr
|
106
|
+
|
107
|
+
Resolv::DNS.open(:timeout => connect_timeout) do |dns|
|
108
|
+
dns.getaddress
|
109
|
+
end
|
110
|
+
|
111
|
+
rescue Resolv::ResolvTimeout
|
112
|
+
raise TimeoutError, "DNS timed out after #{connect_timeout} seconds"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
# rubocop:enable Lint/HandleExceptions
|
data/lib/http/version.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require "support/
|
1
|
+
require "support/http_handling_shared"
|
2
2
|
require "support/dummy_server"
|
3
3
|
require "http/cache"
|
4
4
|
|
@@ -167,44 +167,34 @@ RSpec.describe HTTP::Client do
|
|
167
167
|
end
|
168
168
|
end
|
169
169
|
|
170
|
-
include_context "
|
171
|
-
let(:
|
172
|
-
let(:keep_alive_timeout) { 5 }
|
173
|
-
|
170
|
+
include_context "HTTP handling" do
|
171
|
+
let(:options) { {} }
|
174
172
|
let(:server) { dummy }
|
175
|
-
let(:client)
|
176
|
-
described_class.new(
|
177
|
-
:persistent => reuse_conn,
|
178
|
-
:keep_alive_timeout => keep_alive_timeout
|
179
|
-
)
|
180
|
-
end
|
173
|
+
let(:client) { described_class.new(options) }
|
181
174
|
end
|
182
175
|
|
183
176
|
describe "SSL" do
|
184
|
-
let(:reuse_conn) { nil }
|
185
|
-
let(:keep_alive_timeout) { 5 }
|
186
|
-
|
187
177
|
let(:client) do
|
188
178
|
described_class.new(
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
179
|
+
options.merge(
|
180
|
+
:ssl_context => OpenSSL::SSL::SSLContext.new.tap do |context|
|
181
|
+
context.options = OpenSSL::SSL::SSLContext::DEFAULT_PARAMS[:options]
|
182
|
+
|
183
|
+
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
184
|
+
context.ca_file = File.join(certs_dir, "ca.crt")
|
185
|
+
context.cert = OpenSSL::X509::Certificate.new(
|
186
|
+
File.read(File.join(certs_dir, "client.crt"))
|
187
|
+
)
|
188
|
+
context.key = OpenSSL::PKey::RSA.new(
|
189
|
+
File.read(File.join(certs_dir, "client.key"))
|
190
|
+
)
|
191
|
+
context
|
192
|
+
end
|
193
|
+
)
|
204
194
|
)
|
205
195
|
end
|
206
196
|
|
207
|
-
include_context "
|
197
|
+
include_context "HTTP handling", true do
|
208
198
|
let(:server) { dummy_ssl }
|
209
199
|
end
|
210
200
|
|
@@ -34,10 +34,13 @@ RSpec.describe HTTP::Options, "merge" do
|
|
34
34
|
:json => {:bar => "bar"},
|
35
35
|
:keep_alive_timeout => 10,
|
36
36
|
:headers => {:accept => "xml", :bar => "bar"},
|
37
|
+
:timeout_options => {:foo => :bar},
|
37
38
|
:proxy => {:proxy_address => "127.0.0.1", :proxy_port => 8080})
|
38
39
|
|
39
40
|
expect(foo.merge(bar).to_hash).to eq(
|
40
41
|
:response => :parsed_body,
|
42
|
+
:timeout_class => described_class.default_timeout_class,
|
43
|
+
:timeout_options => {:foo => :bar},
|
41
44
|
:params => {:plop => "plip"},
|
42
45
|
:form => {:bar => "bar"},
|
43
46
|
:body => "body-bar",
|
@@ -40,6 +40,20 @@ class DummyServer < WEBrick::HTTPServer
|
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
|
+
get "/sleep" do |_, res|
|
44
|
+
sleep 2
|
45
|
+
|
46
|
+
res.status = 200
|
47
|
+
res.body = "hello"
|
48
|
+
end
|
49
|
+
|
50
|
+
post "/sleep" do |_, res|
|
51
|
+
sleep 2
|
52
|
+
|
53
|
+
res.status = 200
|
54
|
+
res.body = "hello"
|
55
|
+
end
|
56
|
+
|
43
57
|
["", "/1", "/2"].each do |path|
|
44
58
|
get "/socket#{path}" do |req, res|
|
45
59
|
self.class.sockets << req.instance_variable_get(:@socket)
|
@@ -0,0 +1,207 @@
|
|
1
|
+
RSpec.shared_context "HTTP handling" do |ssl = false|
|
2
|
+
describe "timeouts" do
|
3
|
+
let(:conn_timeout) { 1 }
|
4
|
+
let(:read_timeout) { 1 }
|
5
|
+
let(:write_timeout) { 1 }
|
6
|
+
|
7
|
+
let(:options) do
|
8
|
+
{
|
9
|
+
:timeout_class => timeout_class,
|
10
|
+
:timeout_options => {
|
11
|
+
:connect_timeout => conn_timeout,
|
12
|
+
:read_timeout => read_timeout,
|
13
|
+
:write_timeout => write_timeout
|
14
|
+
}
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
context "without timeouts" do
|
19
|
+
let(:timeout_class) { HTTP::Timeout::Null }
|
20
|
+
let(:conn_timeout) { 0 }
|
21
|
+
let(:read_timeout) { 0 }
|
22
|
+
let(:write_timeout) { 0 }
|
23
|
+
|
24
|
+
it "works" do
|
25
|
+
expect(client.get(server.endpoint).body.to_s).to eq("<!doctype html>")
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "with a per operation timeout" do
|
30
|
+
let(:timeout_class) { HTTP::Timeout::PerOperation }
|
31
|
+
|
32
|
+
let(:response) { client.get(server.endpoint).body.to_s }
|
33
|
+
|
34
|
+
it "works" do
|
35
|
+
expect(response).to eq("<!doctype html>")
|
36
|
+
end
|
37
|
+
|
38
|
+
context "connection" do
|
39
|
+
context "of 1" do
|
40
|
+
let(:conn_timeout) { 1 }
|
41
|
+
|
42
|
+
it "does not time out" do
|
43
|
+
expect { response }.to_not raise_error
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "read" do
|
49
|
+
context "of 0" do
|
50
|
+
let(:read_timeout) { 0 }
|
51
|
+
|
52
|
+
it "times out" do
|
53
|
+
expect { response }.to raise_error(HTTP::TimeoutError, /Read/i)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context "of 2.5" do
|
58
|
+
let(:read_timeout) { 2.5 }
|
59
|
+
|
60
|
+
it "does not time out" do
|
61
|
+
expect { client.get("#{server.endpoint}/sleep").body.to_s }.to_not raise_error
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "with a global timeout" do
|
68
|
+
let(:timeout_class) { HTTP::Timeout::Global }
|
69
|
+
|
70
|
+
let(:conn_timeout) { 0 }
|
71
|
+
let(:read_timeout) { 1 }
|
72
|
+
let(:write_timeout) { 0 }
|
73
|
+
|
74
|
+
let(:response) { client.get(server.endpoint).body.to_s }
|
75
|
+
|
76
|
+
context "with localhost" do
|
77
|
+
let(:endpoint) { server.endpoint.sub("127.0.0.1", "localhost") }
|
78
|
+
|
79
|
+
it "errors if DNS takes too long" do
|
80
|
+
# Block the localhost lookup
|
81
|
+
expect(timeout_class::HostResolver)
|
82
|
+
.to receive(:getaddress).with("localhost").and_return(nil)
|
83
|
+
|
84
|
+
# Request
|
85
|
+
expect(Resolv::DNS).to receive(:open).with(:timeout => 1) do |_|
|
86
|
+
sleep 1.25
|
87
|
+
"127.0.0.1"
|
88
|
+
end
|
89
|
+
|
90
|
+
expect { client.get(server.endpoint.sub("127.0.0.1", "localhost")) }
|
91
|
+
.to raise_error(HTTP::TimeoutError, /Timed out/)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
it "errors if connecting takes too long" do
|
96
|
+
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
97
|
+
|
98
|
+
fake_socket = double(:to_io => socket)
|
99
|
+
expect(fake_socket).to receive(:connect_nonblock) do |*args|
|
100
|
+
sleep 1.25
|
101
|
+
socket.connect_nonblock(*args)
|
102
|
+
end
|
103
|
+
|
104
|
+
allow_any_instance_of(timeout_class).to receive(:socket).and_return(fake_socket)
|
105
|
+
|
106
|
+
expect { response }.to raise_error(HTTP::TimeoutError, /Timed out/)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "errors if reading takes too long" do
|
110
|
+
expect { client.get("#{server.endpoint}/sleep").body.to_s }
|
111
|
+
.to raise_error(HTTP::TimeoutError, /Timed out/)
|
112
|
+
end
|
113
|
+
|
114
|
+
unless ssl
|
115
|
+
it "errors if writing takes too long" do
|
116
|
+
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
|
117
|
+
allow_any_instance_of(timeout_class).to receive(:socket).and_return(socket)
|
118
|
+
|
119
|
+
expect(socket).to receive(:<<) do |*|
|
120
|
+
sleep 1.25
|
121
|
+
end
|
122
|
+
|
123
|
+
expect { response }.to raise_error(HTTP::TimeoutError, /Timed out/)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "connection reuse" do
|
130
|
+
let(:sockets_used) do
|
131
|
+
[
|
132
|
+
client.get("#{server.endpoint}/socket/1").body.to_s,
|
133
|
+
client.get("#{server.endpoint}/socket/2").body.to_s
|
134
|
+
]
|
135
|
+
end
|
136
|
+
|
137
|
+
context "when enabled" do
|
138
|
+
let(:options) { {:persistent => server.endpoint} }
|
139
|
+
|
140
|
+
context "without a host" do
|
141
|
+
it "infers host from persistent config" do
|
142
|
+
expect(client.get("/").body.to_s).to eq("<!doctype html>")
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
it "re-uses the socket" do
|
147
|
+
expect(sockets_used).to_not include("")
|
148
|
+
expect(sockets_used.uniq.length).to eq(1)
|
149
|
+
end
|
150
|
+
|
151
|
+
context "when trying to read a stale body" do
|
152
|
+
it "errors" do
|
153
|
+
client.get("#{server.endpoint}/not-found")
|
154
|
+
expect { client.get(server.endpoint) }.to raise_error(HTTP::StateError, /Tried to send a request/)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
context "when reading a cached body" do
|
159
|
+
it "succeeds" do
|
160
|
+
first_res = client.get(server.endpoint)
|
161
|
+
first_res.body.to_s
|
162
|
+
|
163
|
+
second_res = client.get(server.endpoint)
|
164
|
+
|
165
|
+
expect(first_res.body.to_s).to eq("<!doctype html>")
|
166
|
+
expect(second_res.body.to_s).to eq("<!doctype html>")
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context "with a socket issue" do
|
171
|
+
it "transparently reopens" do
|
172
|
+
first_socket = client.get("#{server.endpoint}/socket").body.to_s
|
173
|
+
expect(first_socket).to_not eq("")
|
174
|
+
# Kill off the sockets we used
|
175
|
+
# rubocop:disable Style/RescueModifier
|
176
|
+
DummyServer::Servlet.sockets.each do |socket|
|
177
|
+
socket.close rescue nil
|
178
|
+
end
|
179
|
+
DummyServer::Servlet.sockets.clear
|
180
|
+
# rubocop:enable Style/RescueModifier
|
181
|
+
|
182
|
+
# Should error because we tried to use a bad socket
|
183
|
+
expect { client.get("#{server.endpoint}/socket").body.to_s }.to raise_error(IOError)
|
184
|
+
|
185
|
+
# Should succeed since we create a new socket
|
186
|
+
second_socket = client.get("#{server.endpoint}/socket").body.to_s
|
187
|
+
expect(second_socket).to_not eq(first_socket)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
context "with a change in host" do
|
192
|
+
it "errors" do
|
193
|
+
expect { client.get("https://invalid.com/socket") }.to raise_error(/Persistence is enabled/i)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context "when disabled" do
|
199
|
+
let(:options) { {} }
|
200
|
+
|
201
|
+
it "opens new sockets" do
|
202
|
+
expect(sockets_used).to_not include("")
|
203
|
+
expect(sockets_used.uniq.length).to eq(2)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: http
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.0.
|
4
|
+
version: 0.8.0.pre4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tony Arcieri
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2015-03-
|
13
|
+
date: 2015-03-29 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: http_parser.rb
|
@@ -104,6 +104,9 @@ files:
|
|
104
104
|
- lib/http/response/status.rb
|
105
105
|
- lib/http/response/status/reasons.rb
|
106
106
|
- lib/http/response/string_body.rb
|
107
|
+
- lib/http/timeout/global.rb
|
108
|
+
- lib/http/timeout/null.rb
|
109
|
+
- lib/http/timeout/per_operation.rb
|
107
110
|
- lib/http/version.rb
|
108
111
|
- logo.png
|
109
112
|
- spec/lib/http/cache/headers_spec.rb
|
@@ -138,6 +141,7 @@ files:
|
|
138
141
|
- spec/support/create_certs.rb
|
139
142
|
- spec/support/dummy_server.rb
|
140
143
|
- spec/support/dummy_server/servlet.rb
|
144
|
+
- spec/support/http_handling_shared.rb
|
141
145
|
- spec/support/proxy_server.rb
|
142
146
|
- spec/support/servers/config.rb
|
143
147
|
- spec/support/servers/runner.rb
|
@@ -198,6 +202,7 @@ test_files:
|
|
198
202
|
- spec/support/create_certs.rb
|
199
203
|
- spec/support/dummy_server.rb
|
200
204
|
- spec/support/dummy_server/servlet.rb
|
205
|
+
- spec/support/http_handling_shared.rb
|
201
206
|
- spec/support/proxy_server.rb
|
202
207
|
- spec/support/servers/config.rb
|
203
208
|
- spec/support/servers/runner.rb
|