Recently at EverTrue, a colleague and I were working on bringing up a new service in Rails. As we were working, I pulled in BetterErrors gem as a way to make development easier. If you’re not familiar with the gem, it replaces the default Rails development error pages with much richer ones and even allows you use REPL on the page that you can use to quickly step though the action that errored. It’s a lot easier then adding debugger. Checkout RailsCast #402 for a quick demo.

We were surprised to find that a major issue though; BetterErrors would return 500 responses for every error raised. If you were working on something that used relied on proper error responses (i.e. an API) then this would make development impossible. Instead of abandoning the gem, I decided to fix the issue and improve the project.

Since BetterErrors fundamentally is a middleware, I used debugger, and later ByeBug, to step though the full response process with and without BetterErrors. This helped me understand what was gong on under the hood with Rails and where the handoff was going arry.

When an error is raised in Rails, say from ActiveRecord, etc…

better_errors/lib/better_errors/middleware.rbf692c5e3b5
1
2
3
4
5
6
7
8
9
10
11
12
13
def show_error_page(env)
  type, content = if @error_page
    if text?(env)
      [ 'plain', @error_page.render('text') ]
    else
      [ 'html', @error_page.render ]
    end
  else
    [ 'html', no_errors_page ]
  end

  [500, { "Content-Type" => "text/#{type}; charset=utf-8" }, [content]]
end

Once I found the issue, I wanted to accomplish a few things with the patch:

  1. Generate the proper error codes.
  2. Do it as DRYly as possible, did not want to have to have to configure errors twice.
  3. Make it seamless with Rails, but not dependent.

The patch that I wrote uses ActionDispatch::ExceptionWrapper, if its available, to map the raised exception to a HTTP status. This is how Rails would normally generate the response, but since BetterErrors’ middleware is taking control, it needs to be checked here.

better_errors/lib/better_errors/middleware.rb5208509aff
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def show_error_page(env, exception=nil)
  type, content = if @error_page
    if text?(env)
      [ 'plain', @error_page.render('text') ]
    else
      [ 'html', @error_page.render ]
    end
  else
    [ 'html', no_errors_page ]
  end

  status_code = 500
  if defined? ActionDispatch::ExceptionWrapper
    status_code = ActionDispatch::ExceptionWrapper.new(env, exception).status_code
  end

  [status_code, { "Content-Type" => "text/#{type}; charset=utf-8" }, [content]]
end

This only uses ActionDispatch if its available, otherwise it defaults to the existing behavior. This means that in a Rails environment, whatever the developer is already doing for custom exception handling does not need to change and it will work seamlessly.

I could see this needing to be expanded for other frameworks or finding a more generic way to handle these errors, but for the time being it scratches my particular itch, and I hope others’ as well.