Transforming ActiveRecord validation errors into API consumable errors

我的未来我决定 提交于 2019-12-09 13:37:10

问题


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!