http 0.8.0.pre3 → 0.8.0.pre4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|