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.
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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
1 2 3 4 5 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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 |