【问题标题】:Transforming ActiveRecord validation errors into API consumable errors将 ActiveRecord 验证错误转换为 API 可使用错误
【发布时间】:2017-03-08 02:51:29
【问题描述】:

我正在 Rails 4 中编写一个非常标准的 CRUD RESTful API。不过,我在错误处理方面做得还不够。

假设我有以下模型:

class Book < ActiveRecord::Base
  validates :title, presence: true
end

如果我尝试创建没有标题的图书对象,我会收到以下错误:

{
  "title": [
    "can't be blank"
  ]
}

ActiveRecord 验证旨在与表单一起使用。理想情况下,我想将每个人类可读的验证错误与 API 使用者可以使用的常量进行匹配。所以像:

{
  "title": [
    "can't be blank"
  ],
  "error_code": "TITLE_ERROR"
}

这既可以用于显示用户面临的错误(“标题不能为空”),也可以在其他代码中使用(if response.error_code === TITLE_ERROR...)。 Rails 中是否有任何工具可以解决这个问题?

编辑:这是very similar question from Rails 2 days

【问题讨论】:

  • 你能有一些控制器代码吗? API 和您的应用请求的代码是否相同?因为您向我们展示了您对模型的验证,但我认为您的问题应该在控制器上处理。

标签: ruby-on-rails rest api activerecord


【解决方案1】:

error_codes.yml 上定义您的标准 API 错误,包括 status_codetitledetails 和一个内部 code,然后您可以使用它来提供有关错误的更多信息您的 API 文档。

这是一个基本的例子:

api:
  invalid_resource:
    code: '1'
    status: '400'
    title: 'Bad Request'

not_found:
    code: '2'
    status: '404'
    title: 'Not Found'
    details: 'Resource not found.'

config/initializers/api_errors.rb 上将该 YAML 文件加载到常量中。

API_ERRORS = YAML.load_file(Rails.root.join('doc','error-codes.yml'))['api']

app/controllers/concerns/error_handling.rb 上定义一个可重用的方法以 JSON 格式呈现您的 API 错误:

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

在您的 API 基础控制器上包含关注点,以便它在从它继承的所有控制器上都可用:

include ErrorHandling

然后您将能够在任何这些控制器上使用您的方法:

respond_with_error('not_found') # For standard API errors
respond_with_error('invalid_resource', @user) # For invalid resources

例如,在您的用户控制器上,您可能有以下内容:

def create
  if @user.save(your_api_params)
    # Do whatever your API needs to do
  else
    respond_with_error('invalid_resource', @user)
  end
end

您的 API 将输出的错误如下所示:

# 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."
}

随着 API 的增长,您将能够轻松地在 YAML 文件中添加新的错误代码,并通过这种方法使用它们,避免重复并使您的错误代码在整个 API 中保持一致。

【讨论】:

  • 很高兴你喜欢它!你介意接受答案吗? :)
  • 我还在思考如何将它与验证码结合起来。
  • 哦,这个方法依赖于Active Record Validations,并且在资源无效的情况下将产生的错误列在一个数组中。只需像往常一样在您的模型中定义验证即可。
  • @EightyEight 你有机会尝试一下吗?如果您需要更多信息,请告诉我。
  • 我一定还是遗漏了一些东西。谁调用respond_with_error('invalid_resource', user) ?你建议unless user.save respond_with_error('invalid_resource', user)吗?
【解决方案2】:

试试这个:

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

error_codes 将返回多个错误代码。

【讨论】:

    【解决方案3】:

    您似乎没有考虑多个验证错误。

    在您的示例中,Book 模型只有一个验证,但其他模型可能有更多验证。

    我的答案包含第一个解决方案,该解决方案考虑了多个验证,另一个解决方案仅使用模型上发现的第一个验证错误

    解决方案 1 - 处理多个验证

    将此添加到您的应用程序控制器中

    # 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
    

    注意error_codes 在这种情况下是一个允许多个错误代码的数组。例如:

    {
      "title": [
        "can't be blank"
      ],
      "author": [
        "can't be blank"
      ],
      "error_codes": ["TITLE_ERROR", "AUTHOR_ERROR"]
    }
    

    解决方案 2 - 仅处理第一个验证错误

    如果您真的只想保留一个验证错误,请改用它

    # 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
    

    这会给你一个类似的回应

    {
      "title": [
        "can't be blank"
      ],
      "error_code": "TITLE_ERROR"
    }
    

    即使你有多个错误

    【讨论】:

      【解决方案4】:

      您只有 2 种方法可以实现这一点:要么为验证器(将在验证期间测试错误的组件)编写代码,要么编写渲染器。

      我假设您知道如何编写渲染器,正如@baron816 的回答所建议的那样,并做一些 DRY 以以某种方式概括它。

      让我带你了解验证器技术:

      1- 让我们为您的错误代码创建一个存储,我称它们为custom_error_codes,我假设您可以一次设置多个错误代码,所以我将使用Array(您更改否则)。

      创建模型关注点

      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
      

      然后将关注点添加到您的模型中

      class MyModel < ActiveRecord::Base
        include ErrorCodesConcern
        ...
      end
      

      2- 让我们破解验证器来添加错误代码的标记。首先我们需要查看验证器源代码,它们位于 (activemodel-gem-path)/lib/active_model/validations/

      在你的 app 目录中创建一个 validators 目录,然后创建以下验证器

      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
      

      然后在我们的模型中使用我们的自定义验证器

      class Book < ActiveRecord::Base
        validates :title, custom_presence: true
      end
      

      3- 因此,您必须修改代码正在使用的所有 rails 验证器并创建渲染器(请参阅 @baron816 的答案)并使用模型的 custom_error_codes 值进行响应。

      【讨论】:

      • 谢谢,这更接近我想要实现的目标。有没有办法在不手动扩展现有验证器的情况下重用它们?手动扩展它们太费力了。
      • 也许你可以尝试破解(基于相同的原则)ActiveModel::Errors#add,这似乎在短期内减少了工作量,但从长远来看,我个人觉得破解验证器更安全。它们只是其中的几个,应该需要几分钟才能实现。
      【解决方案5】:

      您的 create 方法应如下所示:

      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
      

      这将返回看起来像您所要求的那样的 json,除了“title”和“error_code”将嵌套在“errors”中。我希望这不是一个大问题。

      【讨论】:

      • 我想概括一下,以便验证错误以某种方式对应于错误代码。想象一下,我添加了一堆其他字段,authorisbnprice
      • 您可以根据需要构建散列,甚至可以嵌套。 render json: { author: book.author, somemore_stuff: { isbn: 12345, price: "$55"}, error_code: "bad stuff", a_thing: a_thing_variable } 会按照你期望的方式转换成json。
      猜你喜欢
      • 2022-08-20
      • 2015-08-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-01-31
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多