How to tell a connect timeout error from a read timeout error in Ruby's Net::HTTP

Here's the solution (after Ben's fix):

require "net/http"
http = Net::HTTP.new("example.com")
http.open_timeout = 2
http.read_timeout = 3
begin
  http.start
  begin
    http.request_get("/whatever?") do |res|
      res.read_body
    end
  rescue Timeout::Error
    puts "Timeout due to reading"
  end
rescue Timeout::Error
  puts "Timeout due to connecting"
end

Marc-André Lafortune's solution is still the best if you can't upgrade to ruby 2.x.

Starting from 2.x, a subclass of Timeout::Error will be raised depending on which timeout was triggered:

  • Net::OpenTimeout
  • Net::ReadTimeout

However, the read_timeout behavior is strange on 2.x, because it seems to double the value you set. This article explains why.

Here's a test for both timeouts (tested on 1.8.7, 1.9.3, 2.1.2, 2.2.4).

EDIT: The open_timeout test works on Mac, but on Linux, the client gets a "connection refused" error.

require "net/http"
require "socket"

SERVER_HOST = '127.0.0.1'
SERVER_PORT = 9999

def main
  puts 'with_nonlistening_server'
  with_nonlistening_server do
    make_request
  end
  
  puts
  puts 'with_listening_server'
  with_listening_server do
    make_request
  end
end

def with_listening_server
  # This automatically starts listening
  serv = TCPServer.new(SERVER_HOST, SERVER_PORT)
  begin
    yield
  ensure
    serv.close
  end
end

def with_nonlistening_server
  raw_serv = Socket.new Socket::AF_INET, Socket::SOCK_STREAM, 0
  addr     = Socket.pack_sockaddr_in SERVER_PORT, SERVER_HOST

  # Bind, but don't listen
  raw_serv.bind addr
  begin
    yield
  ensure
    raw_serv.close
  end
end

def make_request
  http = Net::HTTP.new(SERVER_HOST, SERVER_PORT)
  http.open_timeout = 1
  http.read_timeout = 1  # seems to be doubled on ruby 2.x
  start_tm = Time.now
  begin
    http.start
    begin
      http.get('/')
    rescue Timeout::Error => err
      puts "Read timeout: #{err.inspect}"
    end
  rescue Timeout::Error => err
    puts "Open timeout: #{err.inspect}"
  end
  end_tm = Time.now
  puts "Duration (sec): #{end_tm - start_tm}"
end

if __FILE__ == $PROGRAM_NAME
  main
end

Example output on 1.9.3:

with_nonlistening_server
Open timeout: #<Timeout::Error: execution expired>
Duration (sec): 1.002477

with_listening_server
Read timeout: #<Timeout::Error: Timeout::Error>
Duration (sec): 1.00599

Example output on 2.1.2:

with_nonlistening_server
Open timeout: #<Net::OpenTimeout: execution expired>
Duration (sec): 1.005923

with_listening_server
Read timeout: #<Net::ReadTimeout: Net::ReadTimeout>
Duration (sec): 2.009582

Tags:

Ruby