0

I spent one day trying several approaches, but still haven't quite got there so decided to ask now...

I have a Rails 5 app which is mainly a JSON API (using the actual JSON API specs), but also a "normal" Rails app with transactional emails and account related pages (reset password, etc).

What I'd like to achieve is that Rails always returns a JSON response with some meaningful error response to all API calls, rather than the default HTML error page or a header only 400 error.

The main cases I'm trying to handle are JSON parsing issues and Ruby exceptions (500 errors).

I tried:

  1. using rescue_from on the ActionController level – seems the framework handles these exceptions before they would reach the controller
  2. Handling them on the Rack level with a middleware – this worked in test but not in dev despite setting consider_all_requests_local to false in both
  3. Registering a new Mime-type and a parser as JSON API Resources gem does it – looked promising, but the parser code is never hit

I'm really at my wit's end, something which sounded so simple ended up being deceptively complicated with me trying to hunt down where are these exceptions get handled in the framework without much success...

dain
  • 6,475
  • 1
  • 38
  • 47

1 Answers1

1

Well I managed to work it out in the end, so thought I should share what worked.

What I missed before is that I had to fiddle a bit with MIME types so that Rails would understand and properly use JSON API:

config/initializers/mime_types.rb

JSON_API_MIME_TYPES = %w[
  application/vnd.api+json
  text/x-json
  application/json
].freeze

Mime::Type.unregister :json
Mime::Type.register 'application/json', :json, JSON_API_MIME_TYPES
Mime::Type.register_alias 'application/json', :json, %i[json_api jsonapi]

After this I could finally handle 500 errors in the base controller:

rescue_from StandardError,
            with: :render_standard_error

def render_standard_error
    render json: {
      status: 500,
      error: 'Unhandled error',
      message: 'An unexpected error occurred.'
    }, status: :internal_server_error
  end

Then for handling JSON parse errors, this was the solution:

app/middleware/catch_json_parse_errors

class CatchJsonParseErrors
  def initialize(app)
    @app = app
  end

  def call(env)
    @app.call(env)
  rescue ActionDispatch::Http::Parameters::ParseError => error
    if JSON_API_MIME_TYPES.include?(env['CONTENT_TYPE']) ||
       JSON_API_MIME_TYPES.include?(env['HTTP_ACCEPT'])
      return [
        400, { 'Content-Type' => 'application/json' },
        [{
          status: 400,
          error: 'JSON parse error',
          message: "There was a problem in the JSON you submitted: #{error}"
        }.to_json]
      ]
    else
      raise error
    end
  end
end

config/application.rb

require './app/middleware/catch_json_parse_errors'
...
config.middleware.use CatchJsonParseErrors
dain
  • 6,475
  • 1
  • 38
  • 47