39

I have a "software as a service" app that uses JSON communicated via a RESTful API.

Simply stated: what are the best practices for capturing and reporting exceptions when using a RESTful API with JSON data interchange?

My first thought was to see what Rails does by generating a scaffold, but that's clearly not right. Here's an excerpt:

class MumblesController < ApplicationController

  # GET /mumbles/1
  # GET /mumbles/1.json
  def show
    @mumble = Mumble.find(params[:id])
    respond_to do |format|
      format.html # show.html.erb
      format.json { render json: @mumble }
    end
  end

end

In this case, if the JSON code sends a non-existent ID, e.g.

http://www.myhost.com/mumbles/99999.json

then Mumble.find() will raise ActiveRecord::RecordNotFound. ActionController will catch that and render an error page in HTML. But HTML is useless to the client that is expecting JSON.

I could work around that by wrapping the Mumble.find() in a begin ... rescue RuntimeError block and rendering a JSON status => :unprocessable_entity or something.

But then what if the client's app sends an invalid path, e.g.:

http://www.myhost.com/badtypo/1.json

Is a JSON based app supposed to catch that and return an error in JSON? If so, where do I capture that without digging deep into ActionDispatch?

So overall, do I punt and let ActionController generate HTML if there's an error? That doesn't feel right...

fearless_fool
  • 33,645
  • 23
  • 135
  • 217

4 Answers4

78

(I found the answer just before I hit [Post your question]. But this might help someone else as well...)

Use ActionController's rescue_from

The answer is to use ActionController's rescue_from, as described in this Guide and documented here. In particular, you can replace the default rendering of the default 404.html and 500.html files along these lines:

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found

private
  def record_not_found(error)
    render :json => {:error => error.message}, :status => :not_found
  end 
end
fearless_fool
  • 33,645
  • 23
  • 135
  • 217
  • 1
    beware: at least in Rails 4, the order of matching in `rescue_from` is *reversed* from regular `rescue`: if you want a different behaviors for a subclass vs. its parent class, put the subclass *after* the parent `:-/` – AlexChaffee Jul 12 '16 at 15:38
7

If it helps anyone, this is what I did as a catch all for my purely json api:

In your ApplicationController that each specific controller inherits from, add

# app/controllers/api/v1/application_controller.rb

# ...

rescue_from StandardError do |exception|
    render json: { :error => exception.message }, :status => 500
end

# ...
  • based mostly off fearless_fool's answer
Tim S
  • 5,023
  • 1
  • 34
  • 34
7

As a developer, you will also want to see traces (preferably with useful lines, filtering out gems). And make traces invisible for production:

  rescue_from StandardError do |exception|
    # Handle only JSON requests
    raise unless request.format.json?

    err = {error: exception.message}

    err[:backtrace] = exception.backtrace.select do |line|
      # filter out non-significant lines:
      %w(/gems/ /rubygems/ /lib/ruby/).all? do |litter|
         not line.include?(litter)
      end
    end if Rails.env.development? and exception.is_a? Exception

    # duplicate exception output to console:
    STDERR.puts ['ERROR:', err[:error], '']
                    .concat(err[:backtrace] || []).join "\n"

    render :json => err, :status => 500
  end
Daniel Garmoshka
  • 5,849
  • 39
  • 40
1

There is no clear consensus on how to keep a consistent standard for writing JSON API code, but this is a some of what I practice (more than what you asked for):

  1. Keep it simple - Try to stay restful. Custom methods can make things complex fast.
  2. Have the server return native error codes, and use 'rescue_from' to capture, and
  3. in other cases, render Rails HTTP response codes, that can be specifically targeted by the client app.

In your case, you might find the Rails respond_to and respond_with handle html/json/other responses with grace. And even in your solution, it still will effectively render the HTML, but that is not what will be interpreted by your client app, which will instead read the HTTP header and get the HTTP response code, which is what is triggering your 'rescue_from'.

Carson Cole
  • 4,183
  • 6
  • 25
  • 35