summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS9
-rw-r--r--lib/net/http.rb17
-rw-r--r--lib/net/protocol.rb39
-rw-r--r--test/net/http/test_http.rb24
-rw-r--r--test/net/protocol/test_protocol.rb85
5 files changed, 169 insertions, 5 deletions
diff --git a/NEWS b/NEWS
index 3650f12604..f07d60815c 100644
--- a/NEWS
+++ b/NEWS
@@ -150,6 +150,15 @@ with all sufficient information, see the ChangeLog file or Redmine
* Matrix#antisymmetric?
+* Net
+
+ * New method:
+
+ * Add write_timeout keyword argument to Net::BufferedIO.new. [Feature #13396]
+
+ * Add Net::BufferedIO#write_timeout, Net::BufferedIO#write_timeout=,
+ Net::HTTP#write_timeout, and Net::HTTP#write_timeout=. [Feature #13396]
+
* REXML
* Improved some XPath implementations:
diff --git a/lib/net/http.rb b/lib/net/http.rb
index 2d575aeecc..bfd0001422 100644
--- a/lib/net/http.rb
+++ b/lib/net/http.rb
@@ -575,7 +575,7 @@ module Net #:nodoc:
#
# _opt_ sets following values by its accessor.
# The keys are ca_file, ca_path, cert, cert_store, ciphers,
- # close_on_empty_response, key, open_timeout, read_timeout, ssl_timeout,
+ # close_on_empty_response, key, open_timeout, read_timeout, write_timeout, ssl_timeout,
# ssl_version, use_ssl, verify_callback, verify_depth and verify_mode.
# If you set :use_ssl as true, you can use https and default value of
# verify_mode is set as OpenSSL::SSL::VERIFY_PEER.
@@ -673,6 +673,7 @@ module Net #:nodoc:
@started = false
@open_timeout = 60
@read_timeout = 60
+ @write_timeout = 60
@continue_timeout = nil
@max_retries = 1
@debug_output = nil
@@ -741,6 +742,12 @@ module Net #:nodoc:
# it raises a Net::ReadTimeout exception. The default value is 60 seconds.
attr_reader :read_timeout
+ # Number of seconds to wait for one block to be write (via one write(2)
+ # call). Any number may be used, including Floats for fractional
+ # seconds. If the HTTP object cannot write data in this many seconds,
+ # it raises a Net::WriteTimeout exception. The default value is 60 seconds.
+ attr_reader :write_timeout
+
# Maximum number of times to retry an idempotent request in case of
# Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET,
# Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError,
@@ -763,6 +770,12 @@ module Net #:nodoc:
@read_timeout = sec
end
+ # Setter for the write_timeout attribute.
+ def write_timeout=(sec)
+ @socket.write_timeout = sec if @socket
+ @write_timeout = sec
+ end
+
# Seconds to wait for 100 Continue response. If the HTTP object does not
# receive a response in this many seconds it sends the request body. The
# default value is +nil+.
@@ -944,6 +957,7 @@ module Net #:nodoc:
if use_ssl?
if proxy?
plain_sock = BufferedIO.new(s, read_timeout: @read_timeout,
+ write_timeout: @write_timeout,
continue_timeout: @continue_timeout,
debug_output: @debug_output)
buf = "CONNECT #{@address}:#{@port} HTTP/#{HTTPVersion}\r\n"
@@ -985,6 +999,7 @@ module Net #:nodoc:
D "SSL established"
end
@socket = BufferedIO.new(s, read_timeout: @read_timeout,
+ write_timeout: @write_timeout,
continue_timeout: @continue_timeout,
debug_output: @debug_output)
on_connect
diff --git a/lib/net/protocol.rb b/lib/net/protocol.rb
index 7ec636b384..cf9fc3d434 100644
--- a/lib/net/protocol.rb
+++ b/lib/net/protocol.rb
@@ -77,11 +77,18 @@ module Net # :nodoc:
class ReadTimeout < Timeout::Error; end
+ ##
+ # WriteTimeout, a subclass of Timeout::Error, is raised if a chunk of the
+ # response cannot be read within the read_timeout.
+
+ class WriteTimeout < Timeout::Error; end
+
class BufferedIO #:nodoc: internal use only
- def initialize(io, read_timeout: 60, continue_timeout: nil, debug_output: nil)
+ def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil)
@io = io
@read_timeout = read_timeout
+ @write_timeout = write_timeout
@continue_timeout = continue_timeout
@debug_output = debug_output
@rbuf = ''.b
@@ -89,6 +96,7 @@ module Net # :nodoc:
attr_reader :io
attr_accessor :read_timeout
+ attr_accessor :write_timeout
attr_accessor :continue_timeout
attr_accessor :debug_output
@@ -237,9 +245,32 @@ module Net # :nodoc:
def write0(*strs)
@debug_output << strs.map(&:dump).join if @debug_output
- len = @io.write(*strs)
- @written_bytes += len
- len
+ case len = @io.write_nonblock(*strs, exception: false)
+ when Integer
+ orig_len = len
+ strs.each_with_index do |str, i|
+ @written_bytes += str.bytesize
+ len -= str.bytesize
+ if len == 0
+ if strs.size == i+1
+ return orig_len
+ else
+ strs = strs[i+1..] # rest
+ break
+ end
+ elsif len < 0
+ strs = strs[i..] # str and rest
+ strs[0] = str[len, -len]
+ break
+ else # len > 0
+ # next
+ end
+ end
+ # continue looping
+ when :wait_writable
+ @io.to_io.wait_writable(@write_timeout) or raise Net::WriteTimeout
+ # continue looping
+ end while true
end
#
diff --git a/test/net/http/test_http.rb b/test/net/http/test_http.rb
index d3c2aecb9f..778bf01f8f 100644
--- a/test/net/http/test_http.rb
+++ b/test/net/http/test_http.rb
@@ -529,6 +529,30 @@ module TestNetHTTP_version_1_1_methods
assert_equal data, res.entity
end
+ def test_timeout_during_HTTP_session_write
+ bug4246 = "expected the HTTP session to have timed out but have not. c.f. [ruby-core:34203]"
+
+ th = nil
+ # listen for connections... but deliberately do not read
+ TCPServer.open('localhost', 0) {|server|
+ port = server.addr[1]
+
+ conn = Net::HTTP.new('localhost', port)
+ conn.write_timeout = 0.01
+ conn.open_timeout = 0.1
+
+ th = Thread.new do
+ assert_raise(Net::WriteTimeout) {
+ conn.post('/', "a"*5_000_000)
+ }
+ end
+ assert th.join(10), bug4246
+ }
+ ensure
+ th.kill
+ th.join
+ end
+
def test_timeout_during_HTTP_session
bug4246 = "expected the HTTP session to have timed out but have not. c.f. [ruby-core:34203]"
diff --git a/test/net/protocol/test_protocol.rb b/test/net/protocol/test_protocol.rb
index 048526b1c7..c1837ecf20 100644
--- a/test/net/protocol/test_protocol.rb
+++ b/test/net/protocol/test_protocol.rb
@@ -26,4 +26,89 @@ class TestProtocol < Test::Unit::TestCase
assert_equal("\u3042\r\n.\r\n", sio.string)
end
end
+
+ def create_mockio
+ mockio = Object.new
+ mockio.instance_variable_set(:@str, +'')
+ mockio.instance_variable_set(:@capacity, 100)
+ def mockio.string; @str; end
+ def mockio.to_io; self; end
+ def mockio.wait_writable(sec); sleep sec; false; end
+ def mockio.write_nonblock(*strs, exception: true)
+ if @capacity <= @str.bytesize
+ if exception
+ raise Net::WaitWritable
+ else
+ return :wait_writable
+ end
+ end
+ len = 0
+ strs.each do |str|
+ len1 = @str.bytesize
+ break if @capacity <= len1
+ @str << str[0, @capacity - @str.bytesize]
+ len2 = @str.bytesize
+ len += len2 - len1
+ end
+ len
+ end
+ mockio
+ end
+
+ def test_write0_timeout
+ mockio = create_mockio
+ io = Net::BufferedIO.new(mockio)
+ io.write_timeout = 0.1
+ assert_raise(Net::WriteTimeout){ io.write("a"*1000) }
+ end
+
+ def test_write0_success
+ mockio = create_mockio
+ io = Net::BufferedIO.new(mockio)
+ io.write_timeout = 0.1
+ len = io.write("a"*10)
+ assert_equal "a"*10, mockio.string
+ assert_equal 10, len
+ end
+
+ def test_write0_success2
+ mockio = create_mockio
+ io = Net::BufferedIO.new(mockio)
+ io.write_timeout = 0.1
+ len = io.write("a"*100)
+ assert_equal "a"*100, mockio.string
+ assert_equal 100, len
+ end
+
+ def test_write0_success_multi1
+ mockio = create_mockio
+ io = Net::BufferedIO.new(mockio)
+ io.write_timeout = 0.1
+ len = io.write("a"*50, "a"*49)
+ assert_equal "a"*99, mockio.string
+ assert_equal 99, len
+ end
+
+ def test_write0_success_multi2
+ mockio = create_mockio
+ io = Net::BufferedIO.new(mockio)
+ io.write_timeout = 0.1
+ len = io.write("a"*50, "a"*50)
+ assert_equal "a"*100, mockio.string
+ assert_equal 100, len
+ end
+
+ def test_write0_timeout_multi1
+ mockio = create_mockio
+ io = Net::BufferedIO.new(mockio)
+ io.write_timeout = 0.1
+ assert_raise(Net::WriteTimeout){ io.write("a"*50,"a"*51) }
+ end
+
+ def test_write0_timeout_multi2
+ mockio = create_mockio
+ io = Net::BufferedIO.new(mockio)
+ io.write_timeout = 0.1
+ assert_raise(Net::WriteTimeout){ io.write("a"*50,"a"*50,"a") }
+ end
end