Bug #11526 » trowe-net-http-idempotent-retry-fix.diff
lib/net/http.rb | ||
---|---|---|
if proxy_user()
|
||
req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl?
|
||
end
|
||
req.retry_networking_errors = false if block_given?
|
||
req.set_body_internal body
|
||
res = transport_request(req, &block)
|
||
if sspi_auth?(res)
|
||
... | ... | |
# avoid a dependency on OpenSSL
|
||
defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError,
|
||
Timeout::Error => exception
|
||
if count == 0 && IDEMPOTENT_METHODS_.include?(req.method)
|
||
if count == 0 && req.retry_networking_errors
|
||
count += 1
|
||
req.body_stream.rewind if req.body_stream
|
||
@socket.close if @socket and not @socket.closed?
|
||
D "Conn close because of error #{exception}, and retry"
|
||
retry
|
lib/net/http/generic_request.rb | ||
---|---|---|
def initialize(m, reqbody, resbody, uri_or_path, initheader = nil)
|
||
@method = m
|
||
@retry_networking_errors = Net::HTTP::IDEMPOTENT_METHODS_.include?(m)
|
||
@request_has_body = reqbody
|
||
@response_has_body = resbody
|
||
... | ... | |
# themselves.
|
||
attr_reader :decode_content
|
||
# When true, this request may be retried if a networking error is
|
||
# encountered. Set to false to disable retries. Defaults to
|
||
# true for idempotent HTTP methods.
|
||
attr_accessor :retry_networking_errors
|
||
def inspect
|
||
"\#<#{self.class} #{@method}>"
|
||
end
|
||
... | ... | |
attr_reader :body_stream
|
||
def body_stream=(input)
|
||
@retry_networking_errors = input.respond_to?(:rewind)
|
||
@body = nil
|
||
@body_stream = input
|
||
@body_data = nil
|
test/net/http/test_http_retries.rb | ||
---|---|---|
# coding: US-ASCII
|
||
require 'test/unit'
|
||
require 'net/http'
|
||
require 'stringio'
|
||
require 'socket'
|
||
class TestNetHTTPIdempotentRetries < Test::Unit::TestCase
|
||
class SimpleHttpServer
|
||
def initialize
|
||
@address = '127.0.0.1'
|
||
@port = 3000
|
||
@handlers = []
|
||
end
|
||
attr_reader :address, :port
|
||
# The given proc should accept the follow block arguments:
|
||
#
|
||
# * `http_method` - String
|
||
# * `request_uri` - String
|
||
# * `request_headers` - Hash<String,String>
|
||
# * `socket` - Socket
|
||
#
|
||
# The block is expected to read the request body and to write
|
||
# the full response.
|
||
def handle(&handler)
|
||
@handlers << Proc.new
|
||
end
|
||
def start
|
||
@server = TCPServer.new(@address, @port)
|
||
@thread = Thread.new do
|
||
loop do
|
||
begin
|
||
socket = @server.accept
|
||
method, uri, _ = socket.gets.split(/\s+/)
|
||
@handlers.shift.call(method, uri, headers(socket), socket)
|
||
socket.close unless socket.closed?
|
||
rescue
|
||
@server.close
|
||
@server = TCPServer.new(@address, @port)
|
||
retry
|
||
end
|
||
end
|
||
end
|
||
end
|
||
def stop
|
||
@thread.kill
|
||
@server.close
|
||
end
|
||
private
|
||
def headers(socket)
|
||
headers = {}
|
||
line = socket.gets
|
||
until line == "\r\n"
|
||
key, value = line.split(/:\s*/, 2)
|
||
headers[key.downcase] = value
|
||
line = socket.gets
|
||
end
|
||
headers
|
||
end
|
||
end
|
||
def setup
|
||
@server = SimpleHttpServer.new
|
||
end
|
||
def teardown
|
||
@server.stop
|
||
end
|
||
def test_idempotent_retry_default
|
||
@server.handle do |_, _, headers, socket|
|
||
socket.close # close without responding
|
||
end
|
||
@server.handle do |_, _, headers, socket|
|
||
socket.print("HTTP/1.1 200 OK\r\n")
|
||
socket.print("\r\n")
|
||
end
|
||
@server.start
|
||
req = Net::HTTP::Get.new('/')
|
||
http = Net::HTTP.new(@server.address, @server.port)
|
||
res = http.request(req)
|
||
assert_equal('200', res.code)
|
||
end
|
||
def test_retry_can_be_disabled
|
||
@server.handle do |_, _, headers, socket|
|
||
socket.close # close without responding
|
||
end
|
||
@server.handle do |_, _, headers, socket|
|
||
socket.print("HTTP/1.1 200 OK\r\n")
|
||
socket.print("\r\n")
|
||
end
|
||
@server.start
|
||
req = Net::HTTP::Get.new('/')
|
||
req.retry_networking_errors = false
|
||
http = Net::HTTP.new(@server.address, @server.port)
|
||
assert_raises {
|
||
http.request(req)
|
||
}
|
||
end
|
||
def test_idempotent_retry_rewinds_put_body_stream
|
||
@server.handle do |_, _, headers, socket|
|
||
# intentionally read some of the data, not all
|
||
socket.read(headers['content-length'].to_i / 2)
|
||
socket.close
|
||
end
|
||
@server.handle do |_, _, headers, socket|
|
||
body = socket.read(headers['content-length'].to_i)
|
||
socket.print("HTTP/1.1 200 OK\r\n")
|
||
socket.print("Content-Length: #{body.size}\n")
|
||
socket.print("\r\n")
|
||
socket.print(body)
|
||
end
|
||
@server.start
|
||
body = StringIO.new('io-body')
|
||
req = Net::HTTP::Put.new('/', 'Content-Length' => body.size.to_s )
|
||
req.body_stream = body
|
||
http = Net::HTTP.new(@server.address, @server.port)
|
||
http.read_timeout = 0.5
|
||
assert_nothing_raised {
|
||
http.request(req)
|
||
}
|
||
end
|
||
def test_idempotent_retry_disabled_with_request_block
|
||
one_meg = 1024 * 1024
|
||
@server.handle do |_, _, headers, socket|
|
||
socket.print("HTTP/1.1 200 OK\r\n")
|
||
socket.print("Content-Length: #{one_meg}\n")
|
||
socket.print("\r\n")
|
||
socket.print('.' * (one_meg / 2))
|
||
raise 'error' # forcefully close the connection
|
||
end
|
||
@server.handle do |_, _, headers, socket|
|
||
socket.print("HTTP/1.1 200 OK\r\n")
|
||
socket.print("Content-Length: #{one_meg}\n")
|
||
socket.print("\r\n")
|
||
socket.print('.' * one_meg)
|
||
end
|
||
@server.start
|
||
http = Net::HTTP.new(@server.address, @server.port)
|
||
http.read_timeout = 0.5
|
||
yield_count = 0
|
||
byte_count = 0
|
||
assert_raises {
|
||
http.request(Net::HTTP::Get.new('/')) do |resp|
|
||
yield_count += 1
|
||
resp.read_body do |bytes|
|
||
byte_count += bytes.bytesize
|
||
end
|
||
end
|
||
}
|
||
assert_equal(one_meg / 2, byte_count)
|
||
assert_equal(1, yield_count)
|
||
end
|
||
end
|