Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails proper way to display error with jbuilder

I am looking to display an error message in a jbuilder view. For instance, one route I might have might be:

/foos/:id/bars

If :id submitted by the user does not exist or is invalid, I'd like to be able to display the error message accordingly in my index.json.builder file.

Using Rails, what's the best way to get this done? The controller might have something such as:

def index
  @bar = Bar.where(:foo_id => params[:id])
end

In this case, params[:id] might be nil, or that object might not exist. I'm not sure whether the best thing to do here is handle it in the controller and explicitly render an error.json.builder, or handle it in the index.json.builder view itself. What's the correct way to do this and if it's in the index.json.builder, is params[:id] available to check there? I know I can see if @bar.nil? but not sure on the inverse?

like image 348
randombits Avatar asked Apr 04 '13 20:04

randombits


2 Answers

I would render index.json.builder or just inline json with :error => 'not found' And don't forget to set proper HTTP status: :status => 404

So result could look like this:

render :json => { :error => 'not found' }, :status => 422 if @bar.nil?
like image 138
Shkarik Avatar answered Oct 17 '22 02:10

Shkarik


I think you meant show, since index is really for lists/collections. And you should get .first on the where, otherwise you just have a relation, right? Then, use .first! to raise an error, because Rails' Rack middleware in Rails 4 public_exceptions will handle is in a basic fashion, e.g.

def show
  # need to do to_s on params value if affected by security issue CVE-2013-1854
  @bar = Bar.where(:foo_id => params[:id].to_s).first!
end

You can also use @bar = Bar.find(params[:id]), but that is deprecated and will be removed in Rails 4.1, after which you would have to add gem 'activerecord-deprecated_finders' to your Gemfile to use.

For index, you'd probably want @bars = Bar.all. If for some reason you want to filter and don't want to scope, etc., then you could use @bars = Bar.where(...).to_a or similar.

Rails 4: Basic Exception Handling in Rack Is Automatic

As long as the query kicks off an error, Rails 4 should be able to return the message part of the error for any supported format where to_(format) can be called on a hash (e.g. json, xml, etc.).

To see why, take a look at Rails' Rack public_exceptions middleware.

If it is html, it is going to try to read in the related file from the public directory in Rails for the status code (e.g. 500.html for a server error/HTTP 500).

If it is some other format, it will try to do to_(the format) on the hash: { :status => status, :error => exception.message }. To see how this would work go to Rails' console:

$ rails c
...
1.9.3p392 :001 > {status: 500, error: "herro shraggy!"}.to_xml
 => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hash>\n  <status type=\"integer\">500</status>\n  <error>herro shraggy!</error>\n</hash>\n" 
1.9.3p392 :002 > {status: 500, error: "herro shraggy!"}.to_json
 => "{\"status\":500,\"error\":\"herro shraggy!\"}" 

In the middleware, you'll see the X-Cascade header in the code and in various places related to Rails' exception handling in Rack. Per this answer, the X-Cascade header is set to pass to tell Rack to try other routes to find a resource.

Rails 3.2.x: Can Handle Exceptions in Rack

In Rails 3.2.x, that code to do to_(format) for the response body, etc. is not in public_exceptions.rb. It only handles html format.

Perhaps you could try replacing the old middleware with the newer version via a patch.

If you'd rather have Rack handle your error in a more specific way without a patch, see #3 in José Valim's post, "My five favorite “hidden” features in Rails 3.2".

In that and as another answer also mentions, you can use config.exceptions_app = self.routes. Then with routes that point to a custom controller, you can handle the errors from any controller like any other request. Note the bit about config.consider_all_requests_local = false in your config/environments/development.rb.

You don't have to use routes to use exceptions_app. Although it may be a little intimidating, it is just a proc/lambda that takes a hash and returns an array whose format is: [http_status_code_number, {headers hash...}, ['the response body']]. For example, you should be able to do this in your Rails 3.2.x config to make it handle errors like Rails 4.0 (this is the latest public_exceptions middleware collapsed):

config.exceptions_app = lambda do |env|
  exception = env["action_dispatch.exception"]
  status = env["PATH_INFO"][1..-1]
  request = ActionDispatch::Request.new(env)
  content_type = request.formats.first
  body = { :status => status, :error => exception.message }
  format = content_type && "to_#{content_type.to_sym}"
  if format && body.respond_to?(format)
    formatted_body = body.public_send(format)
    [status, {'Content-Type' => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
            'Content-Length' => body.bytesize.to_s}, [formatted_body]]
  else
    found = false
    path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale
    path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path))

    if found || File.exist?(path)
      [status, {'Content-Type' => "text/html; charset=#{ActionDispatch::Response.default_charset}",
              'Content-Length' => body.bytesize.to_s}, [File.read(path)]]
    else
      [404, { "X-Cascade" => "pass" }, []]
    end
  end
end

Note: For any problem with that handling, the failsafe implementation is in ActionDispatch::ShowExceptions here.

Rails 3 and 4: Handling Some Exceptions in Rails Controller

If you'd rather have error rendering in the controller itself, you can do:

def show
  respond_with @bar = Bar.where(:foo_id => params[:id].to_s).first!
rescue ActiveRecord::RecordNotFound => e
  respond_to do |format|
    format.json => { :error => e.message }, :status => 404
  end
end

But, you don't need to raise errors. You could also do:

def show
  @bar = Bar.where(:foo_id => params[:id].to_s).first
  if @bar
    respond_with @bar
  else
    respond_to do |format|
      format.json => { :error => "Couldn't find Bar with id=#{params[:id]}" }, :status => 404
    end
  end
end

You can also use rescue_from, e.g. in your controller, or ApplicationController, etc.:

rescue_from ActiveRecord::RecordNotFound, with: :not_found

def not_found(exception)
  respond_to do |format|
    format.json => { :error => e.message }, :status => 404
  end
end

or:

rescue_from ActiveRecord::RecordNotFound do |exception|
  respond_to do |format|
    format.json => { :error => e.message }, :status => 404
  end
end

Though some common errors can be handled in the controller, if you errors related to missing routes, etc. formatted in json, etc., those need to be handled in Rack middleware.

like image 5
Gary S. Weaver Avatar answered Oct 17 '22 04:10

Gary S. Weaver