So you decided that you want use your Rails application as an API, huh? Great! But have you thought of the strategy for error handling? No? That’s also great, because I am here to help you!
Handling errors: the basic approach
If you were trying to find out how to handle error responses in Rails application, the chances are that you found something like this:
def update
@article = Article.find(params[:id])
if @article.update_attributes(article_attributes)
render json: @article
else
render json: @article.errors, status: :unprocessable_entity
end
rescue ActiveRecord::RecordNotFound
render json: { error: error.message }, status: :not_found
end
This is OK and might be enough, but it quickly gets ugly and disorganized as our logic, edge cases and possible errors to handle grow.
Handling errors behind the scenes
Since most of the endpoints will need some kind of error handling, our goal should be to reduce boilerplate when adding new features. Even more importantly: this will reduce the risk that someone forgets to handle validation errors. The pattern that I want to have is a “happy path” in your action and handling errors behind the scenes:
def update
@article = Article.find(params[:id])
@article.update!(article_attributes)
end
Moreover, I want to be able to define more error responses easily. There are not too many apps with only 404 and 422 responses.
How to achieve this? I will go through the development process step by step and you can stop as early as you want, even after first step. Every step will work just fine, but the further you go, the more control over error responses and exceptions handling you get.
(In)famous rescue_from
The first step is using Rails’ rescue_from
method. While you should be careful using this method, I think it is fairly safe to say that it can make your life much easier as long as it is properly used (and documented…) and as long as your application’s logic fits its usage pattern.
Using exceptions for flow control is sometimes considered as an anti-pattern, but I think that in this case it is really reasonable. The code executed in the rescue_from
doesn’t have any side effects and its only result is an immediate error response.
In the Rails documentation you can see that rescue_from
lets you define code which will be called when an exception of a given type is raised. Applying this to our example:
class ApplicationController < ActionController::API
rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_response
def render_unprocessable_entity_response(exception)
render json: exception.record.errors, status: :unprocessable_entity
end
def render_not_found_response(exception)
render json: { error: exception.message }, status: :not_found
end
end
class ArticlesController < ApplicationController
def update
@article = Article.find(params[:id])
@article.update!(article_attributes)
end
end
Done! Now the code in the update
action only contains the “happy path” (the article exists and there were no validation errors) and errors are handled in ApplicationController
. How does it work? When the find
method fails to find a record with the given params[:id]
it raises an ActiveRecord::RecordNotFound
exception – which is then rescued in ApplicationController
by calling the render_not_found_response
method. Similarly, using the update!
method (with bang) raises an ActiveRecord::RecordInvalid
exception when the validation doesn’t pass.
So is it ready? For a very simple application it might be enough, but lets find out what it actualy returns. Example responses in case of 404 and 422 errors:
{
"error": "Couldn't find Article with 'id'=1"
}
{
"title": [ "can't be blank" ]
}
Is it OK? If you’re writing a JavaScript application and want to render errors as they are on the page it might be OK. But what if you want to know the type of the validation error and handle processing on the client side? This seems like a reasonable idea, right?
Customizing validation errors
In hobby or small projects you could probably stop here, but when developing most client projects or an open API the chances are that you will have to provide a more specific response format, like this:
{
"message": "Validation Failed",
"errors": [
{
"resource": "Article",
"field": "title",
"code": "blank"
}
]
}
How can we get there? When looking more closely on how rescue works in the case of ActiveRecord::RecordInvalid
, you can see that you can call exception.record
which returns the problematic record. Now you could just iterate through the errors and construct JSON objects with model name, field and message; it sounds tempting, but in most projects this will lead to one of the following:
- you will end up with models and attributes naming influencing your API;
- you will end up with a bunch of
if
s to counter the above.
As both of those scenarios sound horrible, I would suggest making a mechanism encouraging other developers to write new code in a proper way (rather then leaving them with a temptation to write “just another small if
” to change the resource or field name).
We will need a class which will handle conversion from standard Rails messages to exactly what we want:
class ValidationErrorsSerializer
attr_reader :record
def initialize(record)
@record = record
end
def serialize
record.errors.details.map do |field, details|
details.map do |error_details|
ValidationErrorSerializer.new(record, field, error_details).serialize
end
end.flatten
end
end
The thing to notice here is the errors.details
method. Before Rails 5 the only way to get the error code name (e.g. blank
, rather than the full message like can't be blank
) was to hack Rails a bit by using the translation_metadata
method. It is not funny to read I18n code to find this, but even if you are not running Rails 5 yet, there is a way to use the details
method because it was extracted to a gem (which is really great!).
Besides that, the class is fairly simple. record.details
returns a hash with keys being attribute (field) names and values being arrays of hashes with details for each error, e.g. {name: [{error: :blank}, {error: :too_short}]}
. The ValidationErrorsSerializer
just calls ValidationErrorSerializer
for every such hash, additionally passing the record and the field name.
The core logic is in ValidationErrorSerializer
:
class ValidationErrorSerializer
def initialize(record, field, details)
@record = record
@field = field
@details = details
end
def serialize
{
resource: resource,
field: field,
code: code
}
end
private
def resource
I18n.t(
underscored_resource_name,
scope: [:resources],
locale: :api,
default: @record.class.to_s
)
end
def field
I18n.t(
@field,
scope: [:fields, underscored_resource_name],
locale: :api,
default: @field.to_s
)
end
def code
I18n.t(
@details[:error],
scope: [:errors, :codes],
locale: :api,
default: @details[:error].to_s
)
end
def underscored_resource_name
@record.class.to_s.gsub('::', '').underscore
end
end
Pretty good, don’t you think? This class is very simple, but it is quite clever as well. Normally, you would use the I18n.t
method (read more about I18n here) to define translations for different languages. In this case it is a convenient way to have custom model and attribute names in the case where you want to return in the API something different from what’s in your database schema (which is often what should happen).
Each I18n.t
call accepts two parameters to achieve that:
locale
: passing:api
ensures that theapi.yml
file will be used,default
: the default value (in the case when there is no entry in theapi.yml
file for the given model, attribute or code).
An example api.yml
file could look like this:
api:
resources:
user: author
fields:
article:
name: title
errors:
codes:
blank: cant_be_blank
On Rails 5 you might get I18n::InvalidLocale
exception, which is raised when a locale is not on the list of available locales. For previous Rails versions config.i18n.enforce_available_locales
is set to false
by default. This can be fixed by either setting the list of available locales or ignoring locales validation in your application config:
config.i18n.available_locales = [:en, :api]
# or
config.i18n.enforce_available_locales = false
The only thing left to make this work is updating the render_unprocessable_entity_response
method:
def render_unprocessable_entity_response(exception)
render json: {
message: "Validation Failed",
errors: ValidationErrorsSerializer.new(exception.record).serialize
}, status: :unprocessable_entity
end
Customizing other errors
I promised that besides having much less boilerplate, you will be able to define custom error responses easily. This is actually very simple to code; we will only need some basic Ruby features.
When looking at class hierarchy of error classes in Rails, one noticable thing is that all errors inherit from StandardError
. This means that we can define such class ourselves and use it whenever we need to return custom error responses.
Let’s say that we expect the following format for error responses:
{
"message": "Not found",
"code": "not_found"
}
or
{
"message": "User doesn't have admin rights.",
"code": "not_an_admin"
}
In the case of the 404 Not found
error, returning the code
and message
may not make that much difference (since the 404
status of an HTTP request is the only indication you need), but in the case of the 403 Forbidden
response (like in the second example) we may want to have different response messages.
Since 404 Not found
will be returned for ActiveRecord::RecordNotFound
, no new class is needed for this kind of exception, but for the admin example I suggest the following class:
class UserIsNotAnAdmin < StandardError
def http_status
403
end
def code
'not_an_admin'
end
def message
"User doesn't have admin rights."
end
def to_hash
{
message: message,
code: code
}
end
end
Applying this to the current codebase:
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity_response
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found_response
rescue_from UserIsNotAnAdmin, with: :render_error_response
def render_unprocessable_entity_response(exception)
render json: {
message: "Validation Failed",
errors: ValidationErrorsSerializer.new(exception.record).serialize
}, status: :unprocessable_entity
end
def render_not_found_response
render json: { message: "Not found", code: "not_found" }, status: :not_found
end
def render_error_response(exception)
render json: { message: exception.message, code: exception.code }, status: exception.http_status
end
end
Now whenever you want the API to return a 403 response when the user doesn’t have admin rights, all you need to do is write:
def do_something
raise UserIsNotAnAdmin unless user.admin?
# do something
end
When UserIsNotAnAdmin
is raised, the code execution is immediately stopped and the exception is rescued by calling render_error_response
. Notice that this method has a generic name (instead of, e.g., render_not_an_admin_response
). The rescue_from
method can receive any number of class names; since render_error_response
doesn’t really care about what object is passed (as long as it implements the message
and code
methods), we can pass any other class there, like:
rescue_from UserIsNotAnAdmin,
AuthenticationFailed,
AuthenticationAttemptsExceeded,
with: :render_error_response
Is there anything more to be done?
Yes, one thing: testing. I didn’t write anything about testing APIs, but I am preparing a blog post about testing Rails applications in which I will use a similar error handling approach. Other than that, the cases described above cover most of Rails applications I’ve seen in my career. That doesn’t mean every single one will fit, though!