问题
I'm writing a pretty standard CRUD RESTful API in Rails 4. I'm coming up short on error handling though.
Imagine I have the following model:
class Book < ActiveRecord::Base
validates :title, presence: true
end
If I try to create a book object without a title I'll get the following error:
{
"title": [
"can't be blank"
]
}
ActiveRecord validations are designed to be used with Forms. Ideally I'd like to match up each human readable validation error with a constant that can be used by an API consumer. So something like:
{
"title": [
"can't be blank"
],
"error_code": "TITLE_ERROR"
}
This can be both used to display user facing errors ("title can't be blank") and can be used within other code (if response.error_code === TITLE_ERROR...). Is there any tooling for this in Rails?
EDIT: Here's a very similar question from Rails 2 days.
回答1:
On error_codes.yml define your standard API errors, including status_code, title, details and an internal code you can then use to provide further info about the error on your API documentation.
Here's a basic example:
api:
invalid_resource:
code: '1'
status: '400'
title: 'Bad Request'
not_found:
code: '2'
status: '404'
title: 'Not Found'
details: 'Resource not found.'
On config/initializers/api_errors.rb load that YAML file into a constant.
API_ERRORS = YAML.load_file(Rails.root.join('doc','error-codes.yml'))['api']
On app/controllers/concerns/error_handling.rb define a reusable method to render your API errors in JSON format:
module ErrorHandling
def respond_with_error(error, invalid_resource = nil)
error = API_ERRORS[error]
error['details'] = invalid_resource.errors.full_messages if invalid_resource
render json: error, status: error['status']
end
end
On your API base controller include the concern so it's available on all the controllers which inherit from it:
include ErrorHandling
You will then be able to use your method on any of those controllers:
respond_with_error('not_found') # For standard API errors
respond_with_error('invalid_resource', @user) # For invalid resources
For example on your users controller you may have the following:
def create
if @user.save(your_api_params)
# Do whatever your API needs to do
else
respond_with_error('invalid_resource', @user)
end
end
The errors your API will output will look like this:
# For invalid resources
{
"code": "1",
"status": "400",
"title": "Bad Request",
"details": [
"Email format is incorrect"
]
}
# For standard API errors
{
"code": "2",
"status": "404",
"title": "Not Found",
"details": "Route not found."
}
As your API grows, you'll be able to easily add new error codes on your YAML file and use them with this method avoiding duplication and making your error codes consistent across your API.
回答2:
Try this:
book = Book.new(book_params)
if user.save
render json: book, status: 201
else
render json: {
errors: book.errors,
error_codes: book.errors.keys.map { |f| f.upcase + "_ERROR" }
},
status: 422
end
The error_codes will return multiple error codes.
回答3:
Your create method should look just something like this:
def create
book = Book.new(book_params)
if user.save
render json: book, status: 201
else
render json: { errors: book.errors, error_code: "TITLE_ERROR" }, status: 422
end
end
That would return json that looks like what you're asking, except that "title" and "error_code" would be nested within "errors." Not a big issue to deal with I hope.
回答4:
You have only 2 ways of achieving this : either you write code for the validators (components that will test for error during validation) or either you write renderers.
I assume you know how to write renderers as @baron816's answer is suggesting, and do some DRY to somehow generalize it.
Let me walk you through the validator technique:
1- Let's create a storage for your error codes, I call them custom_error_codes and I assume you can set multiple error codes at once so I'll use an Array (you change that otherwise).
Create a model concern
module ErrorCodesConcern
extend ActiveSupport::Concern
included do
# storage for the error codes
attr_reader :custom_error_codes
# reset error codes storage when validation process starts
before_validation :clear_error_codes
end
# default value so the variable is not empty when accessed improperly
def custom_error_codes
@custom_error_codes ||= []
end
private
def clear_error_codes
@custom_error_codes = []
end
end
Then add the concern to your models
class MyModel < ActiveRecord::Base
include ErrorCodesConcern
...
end
2- Let's hack validators to add the marking of error codes. First we need to look at the validators source code, they are located in (activemodel-gem-path)/lib/active_model/validations/.
Create a validators directory in your app directory, then create the following validator
class CustomPresenceValidator < ActiveModel::Validations::PresenceValidator
# this method is copied from the original validator
def validate_each(record, attr_name, value)
if value.blank?
record.errors.add(attr_name, :blank, options)
# Those lines are our customization where we add the error code to the model
error_code = "#{attr_name.upcase}_ERROR"
record.custom_error_codes << error_code unless record.custom_error_codes.include? error_code
end
end
end
Then use our custom validator in our models
class Book < ActiveRecord::Base
validates :title, custom_presence: true
end
3- So you have to modify all rails validators that your code is using and create renderers (see @baron816's answer) and response with the model's custom_error_codes value.
回答5:
It seem's like you are not considering multiple validation errors.
In your example the Book model has just one validation, but other models may have more validations.
My answer contains a first solution that accounts for multiple validations and another solution that uses only the first validation error found on the model
Solution 1 - Handling multiple validations
Add this in your ApplicationController
# Handle validation errors
rescue_from ActiveRecord::RecordInvalid do |exception|
messages = exception.record.errors.messages
messages[:error_codes] = messages.map {|k,v| k.to_s.upcase << "_ERROR" }
render json: messages, status: 422
end
note that error_codes in this case is an array to allow multiple error codes. eg:
{
"title": [
"can't be blank"
],
"author": [
"can't be blank"
],
"error_codes": ["TITLE_ERROR", "AUTHOR_ERROR"]
}
Solution 2 - Handling only the first validation error
If you really want to keep only a single validation error, use this instead
# Handle validation errors
rescue_from ActiveRecord::RecordInvalid do |exception|
key = exception.record.errors.messages.keys[0]
msg = exception.record.errors.messages[key]
render json: { key => msg, :error_code => key.to_s.upcase << "_ERROR" }, status: 422
end
which will give you a response like
{
"title": [
"can't be blank"
],
"error_code": "TITLE_ERROR"
}
even when you have multiple errors
来源:https://stackoverflow.com/questions/40250408/transforming-activerecord-validation-errors-into-api-consumable-errors