Active_model_serializers: JSON API Errors Best practises

Created on 3 Jul 2015  路  12Comments  路  Source: rails-api/active_model_serializers

Hi All!

Is there a built-in way to serialize errors as per the API spec? Or is that left up to the developer - via a custom ErrorSerializer?

TY!

hx

Most helpful comment

Sure! I'm still working on it - but for now this is working nicely. It's not yet setup for actual model attribute errors, but it's a start.

# lib/api/exception.rb
module Api
  class Exception < ::Exception
    attr_accessor :details
    attr_accessor :code

    attr_accessor :status
    attr_accessor :title
    attr_accessor :detail

    def initialize(code, details={})
      @details = details
      @code    = code

      @status = I18n.t "exceptions.#{code}.status"
      @title  = I18n.t "exceptions.#{code}.title", details
      @detail = I18n.t "exceptions.#{code}.detail", details

      @message = @detail
    end

    def as_json
      {
        title: title,
        detail: detail,
        code: code,
        status: status
      }
    end
  end
end

Concern included in API Base Controller

# app/controllers/api/concerns/error_handling.rb
module Api
  module ErrorHandling
    extend ActiveSupport::Concern

    included do
      rescue_from Exception do |error|
        handle_exception error
      end
    end

    protected

    def handle_exception(error=nil)
      case error
      when Api::Exception
        render json: error.as_json, status: error.status.to_sym
      # example of wrapping vendor exceptions
      when CanCan::AccessDenied
        handle_exception Api::Exception.new('permissions.access_denied', {
          action: error.action.to_s,
          subject_id: error.subject.id.to_s,
          subject_class_name: error.subject.class.to_s
        })
      else
        # TODO: Handle this case better
        render json: { message: error.message }, status: :unprocessable_entity
      end
    end
  end
end

I18n for managing error copy, interpolations & HTTP status from a single file

# config/locales/en.yml
en:
  exceptions:
    auth:
      email_is_invalid:
        title: 'Authentication: Email Address is invalid'
        detail: '%{invalid_email} is not a valid email address.'
        status: 'bad_request'

    permissions:
      access_denied:
        title: 'Permissions: Access Denied'
        detail: 'The Current User does not have permission to %{action} a %{subject_class_name} with id: %{subject_id}.'
        status: 'unauthorized'

Then for example, in a controller I just do:

raise Api::Exception.new("auth.email_is_invalid", { invalid_email: params[:email] }) unless EmailValidator.valid?(params[:email])

All 12 comments

Hey @hhff someone might correct me, if wrong, but I'm almost 100% sure there is no built-in way right now :sweat_smile:

If you call:

render json: {error: 'Result is Invalid'}

It will only convert it to json and render it, without using any serializer.

We'd need to add a special error serializer or adapter, or both, since they are different from a normal response. In the short run, returning json that you craft yourself is the best thing to do.

It might be interesting to add an 'error' renderer to the controller, or a rescue_from.

I actually subclass'd Exception, and wrote the render logic into the as_json method, so now I just do:

render json: error.as_json

@bf4 I'm doing this right know. When I'm finished I'll post a gist with the approach.

@hhff Can you share some code? That sounds both interesting and concerning :)

Sure! I'm still working on it - but for now this is working nicely. It's not yet setup for actual model attribute errors, but it's a start.

# lib/api/exception.rb
module Api
  class Exception < ::Exception
    attr_accessor :details
    attr_accessor :code

    attr_accessor :status
    attr_accessor :title
    attr_accessor :detail

    def initialize(code, details={})
      @details = details
      @code    = code

      @status = I18n.t "exceptions.#{code}.status"
      @title  = I18n.t "exceptions.#{code}.title", details
      @detail = I18n.t "exceptions.#{code}.detail", details

      @message = @detail
    end

    def as_json
      {
        title: title,
        detail: detail,
        code: code,
        status: status
      }
    end
  end
end

Concern included in API Base Controller

# app/controllers/api/concerns/error_handling.rb
module Api
  module ErrorHandling
    extend ActiveSupport::Concern

    included do
      rescue_from Exception do |error|
        handle_exception error
      end
    end

    protected

    def handle_exception(error=nil)
      case error
      when Api::Exception
        render json: error.as_json, status: error.status.to_sym
      # example of wrapping vendor exceptions
      when CanCan::AccessDenied
        handle_exception Api::Exception.new('permissions.access_denied', {
          action: error.action.to_s,
          subject_id: error.subject.id.to_s,
          subject_class_name: error.subject.class.to_s
        })
      else
        # TODO: Handle this case better
        render json: { message: error.message }, status: :unprocessable_entity
      end
    end
  end
end

I18n for managing error copy, interpolations & HTTP status from a single file

# config/locales/en.yml
en:
  exceptions:
    auth:
      email_is_invalid:
        title: 'Authentication: Email Address is invalid'
        detail: '%{invalid_email} is not a valid email address.'
        status: 'bad_request'

    permissions:
      access_denied:
        title: 'Permissions: Access Denied'
        detail: 'The Current User does not have permission to %{action} a %{subject_class_name} with id: %{subject_id}.'
        status: 'unauthorized'

Then for example, in a controller I just do:

raise Api::Exception.new("auth.email_is_invalid", { invalid_email: params[:email] }) unless EmailValidator.valid?(params[:email])

Rather than rescue_from Exception do |error| you might want to set Rails.configuration.exceptions_app = Api::ErrorHandling.new(and see https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/public_exceptions.rb and/or maybe def handle_error(exception) and def rescue_with_handler(exception) )

Hey ppl, awesome discussion going on here :smile:
Liked the solution @hhff
Make yourself comfortable to keep the discussion itself, I'm closing the issue for now.

@hhff the correct file path should be
# app/controllers/concerns/api/error_handling.rb

Error handling still be the gap of popular gems for all API steps?

Sure! I'm still working on it - but for now this is working nicely. It's not yet setup for actual model attribute errors, but it's a start.

# lib/api/exception.rb
module Api
  class Exception < ::Exception
    attr_accessor :details
    attr_accessor :code

    attr_accessor :status
    attr_accessor :title
    attr_accessor :detail

    def initialize(code, details={})
      @details = details
      @code    = code

      @status = I18n.t "exceptions.#{code}.status"
      @title  = I18n.t "exceptions.#{code}.title", details
      @detail = I18n.t "exceptions.#{code}.detail", details

      @message = @detail
    end

    def as_json
      {
        title: title,
        detail: detail,
        code: code,
        status: status
      }
    end
  end
end

Some times you raise an exception from business logic layer where you don't know the status code because it is the responsibility of the controller layer. this solution won't work in that case. Or please correct me if I am wrong please

@asad-ali-bhatti three things

  1. you're commenting on an issue closed 4 years ago, which is generally bad form
  2. you're not providing any context for why your issue today relates in any way to code from 4 years ago
  3. your proposal may suit you fine, but 1) it's not good design for non-request-handling code to know what response code to you 2) you inherited from Exception is a super bad idea. Use StandardError
Was this page helpful?
0 / 5 - 0 ratings