Global error handling compatible with the jsonapi.org spec
Add to your ApplicationController:
class ApplicationController < ActionController::Base
include JsonapiErrorable
rescue_from Exception do |e|
handle_exception(e)
end
end
Once installed, all errors will return a valid error response. raise "foo"
would render:
{
errors: [
code: 'internal_server_error',
status: '500',
title: 'Error',
detail: "We've notified our engineers and hope to address this issue shortly.",
meta: {}
]
}
Given a record fails validations, you probably want to render a custom error message specific to the validation failure. Use render_errors_for
:
def create
post = Post.new(post_params)
if post.save
render json: post
else
render_errors_for(post)
end
end
Assuming the Post’s title
was missing, this would render:
{
errors: [
{
code: 'unprocessable_entity',
status: '422',
title: 'Validation Error',
detail: "Title can't be blank",
source: { pointer: '/data/attributes/title' },
meta: {
attribute: 'title',
message: "can't be blank"
code: 'blank'
}
}
]
}
This will work for any PORO including ActiveModel::Validations
Note: ‘meta/code’ is only available in ActiveModel >= 5
We use the meta
section of the error payload handle nested relationships. Let’s say we were sideposting a comment that had a validation error on body
. You’d get back:
{
errors: [
{
code: 'unprocessable_entity',
status: '422',
title: 'Validation Error',
detail: "Body can't be blank",
source: { pointer: '/data/attributes/body' },
meta: {
relationship: {
attribute: 'body',
message: "can't be blank"
code: 'blank'
id: '123',
type: 'comments'
}
}
}
]
}
You can customize an error’s response by using register_exception
in your controller. Let’s say we want ActiveRecord::RecordNotFound
to have status code 404
instead of 500
:
class ApplicationController < ActionController::Base
# ...installation code...
register_exception ActiveRecord::RecordNotFound, status: 404
end
Would now render http status code 404
, with the error JSON containing status: '404'
and code: 'not_found'
.
Available options are:
status
: An http status codetitle
: Custom titlelog
: Pass false
to avoid logging the errormessage
: Pass true
to render the error’s message directly. Alternatively, this can accept a proc, e.g. register_exception FooError, message: ->(e) { e.message.upcase }
You may want to render the actual error message and backtrace - for instance, if the user is an admin, or if Rails.env.staging?
. In this case:
handle_exception(e, show_raw_error: current_user.admin?)
This will add __raw_error__
to the meta
section of the payload, containing the message and backtrace.
The final option register_exception
accepts is handler
. Here you can inject your own error handling class that customizes JsonapiErrorable::ExceptionHandler. For example:
class MyCustomHandler < JsonapiErrorable::ExceptionHandler
def status_code(error)
# ...customize...
end
def error_code(error)
# ...customize...
end
def title
# ...customize...
end
def detail(error)
# ...customize...
end
def meta(error)
# ...customize...
end
def log(error)
# ...customize...
end
end
register_exception FooError, handler: MyCustomHandler
If you would like to use the same custom handler for all errors, override default_exception_handler
:
# app/controllers/application_controller.rb
def self.default_exception_handler
MyCustomHandler
end
All controllers will inherit any registered exceptions from their parent. They can also add their own. In this example, FooError
will only throw a custom status code when thrown from FooController
:
class FooController < ApplicationController
register_exception FooError, status: 422
end
You can assign any logger using JsonapiErrorable.logger = your_logger
You may want your tests to actually raise errors instead of returning error JSON. In this case use disabled!
and enabled
:
before :each do
JsonapiErrorable.disable!
end
it 'renders correct error response' do
JsonapiErrorable.enable! # enabled just for this test
end