Ruby Net::HTTP execution expired

davidgyoung picture davidgyoung · Sep 24, 2016 · Viewed 9.8k times · Source

Using Net::HTTP, I periodically find that the code below rescues from StandardError with a message of "execution expired", despite the fact that the web server logs from the accessed URL show the corresponding response was sent quickly. When the web server logs show a response that takes more than 5 seconds, I generally see the code rescue from Timeout::Error.

What case would cause the code below to rescue from StandardError with "execution expired" instead of rescuing from Timeout::Error?

This code is running in a multi-threaded program on a relatively ancient Ruby 1.9.3, on a platform that does not support newer versions of Ruby. Although the program is multi-threaded, the code shown only runs on a single thread.

begin
  connection = Net::HTTP.new(uri.host, uri.port)
  connection.open_timeout = 5
  connection.read_timeout = 5
  connection.start do |http|
    request = Net::HTTP::Post.new("/reader_events")
    request.body = body
    response = http.request(request)
  end
rescue StandardError => std_error
  log "error sending event to server: #{std_error}"
rescue Timeout::Error => error
  log "timeout sending event to server"
end

Answer

smefju picture smefju · Sep 25, 2016

This is because how rescue works. Take a look at documentation page for Exception class. Basically you can create many exceptions that inherits from a single one, and handle all of them using rescue with the parent class:

begin
  ...
rescue Exception => exception
  ...
end

This code will rescue all types of exceptions as Exception is the root (other exceptions inherits from it). In your case Timeout::Error inherits from RuntimeError which inherits from StandardError:

Timeout::Error.ancestors
  => [Timeout::Error, RuntimeError, StandardError, Exception, Object, PP::ObjectMixin, Kernel, BasicObject]

As the result it is kind of Exception:

Timeout::Error.new.is_a?(StandardError)
  => true

Another thing in your case is that interpreter will check each rescue statement from the top to the bottom. It means that first it will check if the exception is kind of StandardError and later it will move to the following rescue block. You should always list rescue blocks from the most specific to the most general one.

Change the order of rescue blocks to fix the code.