Index: lib/net/http.rb =================================================================== --- lib/net/http.rb (revision 29219) +++ lib/net/http.rb (working copy) @@ -517,6 +517,7 @@ module Net #:nodoc: @started = false @open_timeout = nil @read_timeout = 60 + @continue_timeout = nil @debug_output = nil @use_ssl = false @ssl_context = nil @@ -570,6 +571,16 @@ module Net #:nodoc: @read_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. + attr_reader :continue_timeout + + # Setter for the continue_timeout attribute. + def continue_timeout=(sec) + @socket.continue_timeout = sec if @socket + @continue_timeout = sec + end + # returns true if the HTTP session is started. def started? @started @@ -660,6 +671,7 @@ module Net #:nodoc: end @socket = BufferedIO.new(s) @socket.read_timeout = @read_timeout + @socket.continue_timeout = @continue_timeout @socket.debug_output = @debug_output if use_ssl? if proxy? @@ -1190,12 +1202,15 @@ module Net #:nodoc: def transport_request(req) begin_transport req - req.exec @socket, @curr_http_version, edit_path(req.path) - begin - res = HTTPResponse.read_new(@socket) - end while res.kind_of?(HTTPContinue) - res.reading_body(@socket, req.response_body_permitted?) { - yield res if block_given? + res = catch(:response) { + req.exec @socket, @curr_http_version, edit_path(req.path) + begin + res = HTTPResponse.read_new(@socket) + end while res.kind_of?(HTTPContinue) + res.reading_body(@socket, req.response_body_permitted?) { + yield res if block_given? + } + res } end_transport req, res res @@ -1740,6 +1755,7 @@ module Net #:nodoc: delete 'Transfer-Encoding' supply_default_content_type write_header sock, ver, path + wait_for_continue sock, ver if sock.continue_timeout sock.write body end @@ -1750,6 +1766,7 @@ module Net #:nodoc: end supply_default_content_type write_header sock, ver, path + wait_for_continue sock, ver if sock.continue_timeout if chunked? while s = f.read(1024) sock.write(sprintf("%x\r\n", s.length) << s << "\r\n") @@ -1768,6 +1785,22 @@ module Net #:nodoc: set_content_type 'application/x-www-form-urlencoded' end + ## + # Waits up to the continue timeout for a response from the server provided + # we're speaking HTTP 1.1 and are expecting a 100-continue response. + + def wait_for_continue(sock, ver) + if ver >= '1.1' and @header['expect'] and + @header['expect'].include?('100-continue') then + if IO.select [sock.io], nil, nil, sock.continue_timeout then + res = HTTPResponse.read_new sock + unless res.kind_of?(Net::HTTPContinue) + throw :response, res + end + end + end + end + def write_header(sock, ver, path) buf = "#{@method} #{path} HTTP/#{ver}\r\n" each_capitalized do |k,v| Index: lib/net/protocol.rb =================================================================== --- lib/net/protocol.rb (revision 29219) +++ lib/net/protocol.rb (working copy) @@ -50,12 +50,14 @@ module Net # :nodoc: def initialize(io) @io = io @read_timeout = 60 + @continue_timeout = nil @debug_output = nil @rbuf = '' end attr_reader :io attr_accessor :read_timeout + attr_accessor :continue_timeout attr_accessor :debug_output def inspect Index: test/net/http/test_http.rb =================================================================== --- test/net/http/test_http.rb (revision 29219) +++ test/net/http/test_http.rb (working copy) @@ -372,6 +372,87 @@ class TestNetHTTP_v1_2_chunked < Test::U end end +class TestNetHTTPContinue < Test::Unit::TestCase + CONFIG = { + 'host' => '127.0.0.1', + 'port' => 10081, + 'proxy_host' => nil, + 'proxy_port' => nil, + 'chunked' => true, + } + + include TestNetHTTPUtils + + def logfile + @debug = StringIO.new('') + end + + def mount_proc(&block) + @server.mount('/continue', WEBrick::HTTPServlet::ProcHandler.new(block.to_proc)) + end + + def test_expect_continue + mount_proc {|req, res| + req.continue + res.body = req.query['body'] + } + start {|http| + http.continue_timeout = 0.2 + http.request_post('/continue', 'body=BODY', 'expect' => '100-continue') {|res| + assert_equal('BODY', res.read_body) + } + } + assert_match(/Expect: 100-continue/, @debug.string) + assert_match(/HTTP\/1.1 100 continue/, @debug.string) + end + + def test_expect_continue_timeout + mount_proc {|req, res| + sleep 0.2 + req.continue # just ignored because it's '100' + res.body = req.query['body'] + } + start {|http| + http.continue_timeout = 0 + http.request_post('/continue', 'body=BODY', 'expect' => '100-continue') {|res| + assert_equal('BODY', res.read_body) + } + } + assert_match(/Expect: 100-continue/, @debug.string) + assert_match(/HTTP\/1.1 100 continue/, @debug.string) + end + + def test_expect_continue_error + mount_proc {|req, res| + res.status = 501 + res.body = req.query['body'] + } + start {|http| + http.continue_timeout = 0 + http.request_post('/continue', 'body=ERROR', 'expect' => '100-continue') {|res| + assert_equal('ERROR', res.read_body) + } + } + assert_match(/Expect: 100-continue/, @debug.string) + assert_not_match(/HTTP\/1.1 100 continue/, @debug.string) + end + + def test_expect_continue_error_while_waiting + mount_proc {|req, res| + res.status = 501 + res.body = req.query['body'] + } + start {|http| + http.continue_timeout = 0.5 + http.request_post('/continue', 'body=ERROR', 'expect' => '100-continue') {|res| + assert_equal('ERROR', res.read_body) + } + } + assert_match(/Expect: 100-continue/, @debug.string) + assert_not_match(/HTTP\/1.1 100 continue/, @debug.string) + end +end + =begin class TestNetHTTP_proxy < Test::Unit::TestCase CONFIG = {