Let's say you are using rescue_from in your Rails application to rescue some type of exceptions that are thrown in your application. For example, you want to rescue a ActiveRecord::StatementInvalid
but not every ActiveRecord::StatementInvalid
: just those ActiveRecord::StatementInvalid
exceptions where the exception message matches a defined pattern.
In this specific case, the following code won't work.
class ApplicationController < ActionController::Base
# ...
rescue_from ActiveRecord::StatementInvalid, :with => :rescue_invalid_encoding
protected
def rescue_invalid_encoding
# ...
end
end
This is because a ActiveRecord::StatementInvalid
is a generic error class and the rescue_from
statement will catch any ActiveRecord::StatementInvalid
indistinctly. But you don't want this, so you'll decide to go ahead and use the old-fashioned if school to filter the exception message.
Only exceptions matching given message pattern should be catched. Any other exception should be released (or to use a technical jargon, rethrown).
class ApplicationController < ActionController::Base
# ...
rescue_from ActiveRecord::StatementInvalid do |exception|
if exception.message =~ /invalid byte sequence for encoding/
rescue_invalid_encoding(exception)
else
raise
end
end
protected
def rescue_invalid_encoding(exception)
head :bad_request
end
end
The way you rethrow an exception in Ruby is calling raise
without passing any exception class or message. Ruby will dutifully re-raise the more recent exception.
Unfortunately, the else
statement won't as expected. The exception
is correctly rethrown but it isn't catched by the standard Rails rescue mechanism and the standard exception page is not rendered. Also, the exception is completely invisible to any exception logging platform that relies on rescue_action_in_public such as Hoptoad or Exceptional.
The explanation is simple. To prevent an infinite loop, Rails has a special Failsafe mechanism. When an Exception occurs in the Exception rescue execution, Rails immediately breaks the execution and enter in Failsafe mode.
From the Rails log
Processing ApplicationController#index (for 127.0.0.1 at 2009-11-03 23:30:19) [GET]
Parameters: {"action"=>"index", "controller"=>"welcome"}
/! FAILSAFE /! Wed Nov 03 23:30:19 +0100 2009
Status: 500 Internal Server Error
ActiveRecord::StatementInvalid
In order to pass invoke the standard rescue mechanism you need to manually call the rescue_action_without_handler(exception)
method.
class ApplicationController < ActionController::Base
# ...
rescue_from ActiveRecord::StatementInvalid do |exception|
if exception.message =~ /invalid encoding/
rescue_invalid_encoding(exception)
else
rescue_action_without_handler(exception)
end
end
protected
def rescue_invalid_encoding(exception)
head :bad_request
end
end
A work of warning: This is a Rails internal API so it can change without additional notice in future versions so be sure to create a test suite to prevent problems when upgrading your Rails version.
Here's an example of integrational test.
require 'test_helper'
class RescuableTest < ActionController::IntegrationTest
fixtures :all
test "rescue from ActiveRecord::StatementInvalid" do
MainController.any_instance.expects(:set_locale).raises(ActiveRecord::StatementInvalid, 'PGError: ERROR: invalid byte sequence for encoding "UTF8": 0xed706')
get "/"
assert_response 400
end
test "rescue from ActiveRecord::StatementInvalid with re-raise" do
MainController.any_instance.expects(:set_locale).raises(ActiveRecord::StatementInvalid, 'Global error')
get "/"
assert_response 500
# perhaps you might want to check here additional instance variables
# or flash messages
end
end