Project

General

Profile

Bug #11526 ยป trowe-net-http-idempotent-retry-fix.diff

tdg5 (Danny Guinther), 09/15/2015 05:40 PM

View differences:

lib/net/http.rb
1403 1403
      if proxy_user()
1404 1404
        req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl?
1405 1405
      end
1406
      req.retry_networking_errors = false if block_given?
1406 1407
      req.set_body_internal body
1407 1408
      res = transport_request(req, &block)
1408 1409
      if sspi_auth?(res)
......
1452 1453
             # avoid a dependency on OpenSSL
1453 1454
             defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError,
1454 1455
             Timeout::Error => exception
1455
        if count == 0 && IDEMPOTENT_METHODS_.include?(req.method)
1456
        if count == 0 && req.retry_networking_errors
1456 1457
          count += 1
1458
          req.body_stream.rewind if req.body_stream
1457 1459
          @socket.close if @socket and not @socket.closed?
1458 1460
          D "Conn close because of error #{exception}, and retry"
1459 1461
          retry
lib/net/http/generic_request.rb
9 9

  
10 10
  def initialize(m, reqbody, resbody, uri_or_path, initheader = nil)
11 11
    @method = m
12
    @retry_networking_errors = Net::HTTP::IDEMPOTENT_METHODS_.include?(m)
12 13
    @request_has_body = reqbody
13 14
    @response_has_body = resbody
14 15

  
......
58 59
  # themselves.
59 60
  attr_reader :decode_content
60 61

  
62
  # When true, this request may be retried if a networking error is
63
  # encountered. Set to false to disable retries. Defaults to
64
  # true for idempotent HTTP methods.
65
  attr_accessor :retry_networking_errors
66

  
61 67
  def inspect
62 68
    "\#<#{self.class} #{@method}>"
63 69
  end
......
97 103
  attr_reader :body_stream
98 104

  
99 105
  def body_stream=(input)
106
    @retry_networking_errors = input.respond_to?(:rewind)
100 107
    @body = nil
101 108
    @body_stream = input
102 109
    @body_data = nil
test/net/http/test_http_retries.rb
1
# coding: US-ASCII
2
require 'test/unit'
3
require 'net/http'
4
require 'stringio'
5
require 'socket'
6

  
7
class TestNetHTTPIdempotentRetries < Test::Unit::TestCase
8

  
9
  class SimpleHttpServer
10

  
11
    def initialize
12
      @address = '127.0.0.1'
13
      @port = 3000
14
      @handlers = []
15
    end
16

  
17
    attr_reader :address, :port
18

  
19
    # The given proc should accept the follow block arguments:
20
    #
21
    # * `http_method` - String
22
    # * `request_uri` - String
23
    # * `request_headers` - Hash<String,String>
24
    # * `socket` - Socket
25
    #
26
    # The block is expected to read the request body and to write
27
    # the full response.
28
    def handle(&handler)
29
      @handlers << Proc.new
30
    end
31

  
32
    def start
33
      @server = TCPServer.new(@address, @port)
34
      @thread = Thread.new do
35
        loop do
36
          begin
37
            socket = @server.accept
38
            method, uri, _ = socket.gets.split(/\s+/)
39
            @handlers.shift.call(method, uri, headers(socket), socket)
40
            socket.close unless socket.closed?
41
          rescue
42
            @server.close
43
            @server = TCPServer.new(@address, @port)
44
            retry
45
          end
46
        end
47
      end
48
    end
49

  
50
    def stop
51
      @thread.kill
52
      @server.close
53
    end
54

  
55
    private
56

  
57
    def headers(socket)
58
      headers = {}
59
      line = socket.gets
60
      until line == "\r\n"
61
        key, value = line.split(/:\s*/, 2)
62
        headers[key.downcase] = value
63
        line = socket.gets
64
      end
65
      headers
66
    end
67

  
68
  end
69

  
70
  def setup
71
    @server = SimpleHttpServer.new
72
  end
73

  
74
  def teardown
75
    @server.stop
76
  end
77

  
78
  def test_idempotent_retry_default
79
    @server.handle do |_, _, headers, socket|
80
      socket.close # close without responding
81
    end
82
    @server.handle do |_, _, headers, socket|
83
      socket.print("HTTP/1.1 200 OK\r\n")
84
      socket.print("\r\n")
85
    end
86
    @server.start
87

  
88
    req = Net::HTTP::Get.new('/')
89
    http = Net::HTTP.new(@server.address, @server.port)
90
    res = http.request(req)
91

  
92
    assert_equal('200', res.code)
93
  end
94

  
95
  def test_retry_can_be_disabled
96
    @server.handle do |_, _, headers, socket|
97
      socket.close # close without responding
98
    end
99
    @server.handle do |_, _, headers, socket|
100
      socket.print("HTTP/1.1 200 OK\r\n")
101
      socket.print("\r\n")
102
    end
103
    @server.start
104

  
105
    req = Net::HTTP::Get.new('/')
106
    req.retry_networking_errors = false
107

  
108
    http = Net::HTTP.new(@server.address, @server.port)
109
    assert_raises {
110
      http.request(req)
111
    }
112
  end
113

  
114
  def test_idempotent_retry_rewinds_put_body_stream
115
    @server.handle do |_, _, headers, socket|
116
      # intentionally read some of the data, not all
117
      socket.read(headers['content-length'].to_i / 2)
118
      socket.close
119
    end
120
    @server.handle do |_, _, headers, socket|
121
      body = socket.read(headers['content-length'].to_i)
122
      socket.print("HTTP/1.1 200 OK\r\n")
123
      socket.print("Content-Length: #{body.size}\n")
124
      socket.print("\r\n")
125
      socket.print(body)
126
    end
127
    @server.start
128

  
129
    body = StringIO.new('io-body')
130
    req = Net::HTTP::Put.new('/', 'Content-Length' => body.size.to_s )
131
    req.body_stream = body
132

  
133
    http = Net::HTTP.new(@server.address, @server.port)
134
    http.read_timeout = 0.5
135

  
136
    assert_nothing_raised {
137
      http.request(req)
138
    }
139
  end
140

  
141
  def test_idempotent_retry_disabled_with_request_block
142
    one_meg = 1024 * 1024
143
    @server.handle do |_, _, headers, socket|
144
      socket.print("HTTP/1.1 200 OK\r\n")
145
      socket.print("Content-Length: #{one_meg}\n")
146
      socket.print("\r\n")
147
      socket.print('.' * (one_meg / 2))
148
      raise 'error' # forcefully close the connection
149
    end
150
    @server.handle do |_, _, headers, socket|
151
      socket.print("HTTP/1.1 200 OK\r\n")
152
      socket.print("Content-Length: #{one_meg}\n")
153
      socket.print("\r\n")
154
      socket.print('.' * one_meg)
155
    end
156
    @server.start
157

  
158
    http = Net::HTTP.new(@server.address, @server.port)
159
    http.read_timeout = 0.5
160

  
161
    yield_count = 0
162
    byte_count = 0
163

  
164
    assert_raises {
165
      http.request(Net::HTTP::Get.new('/')) do |resp|
166
        yield_count += 1
167
        resp.read_body do |bytes|
168
          byte_count += bytes.bytesize
169
        end
170
      end
171
    }
172
    assert_equal(one_meg / 2, byte_count)
173
    assert_equal(1, yield_count)
174
  end
175
end