44

I'm trying to build an API in rails 4, and am having an issue where rails returns a 500 error instead of a 406 when using respond_to :json and trying to access the html version.

Here's an example controller demonstrating the problem:

class PostsController < ApplicationController
  respond_to :json

  def index
    @posts = Post.all
  end
end

I also have a jbuilder view for index that works when accessing via JSON. If I try accessing the route without the JSON extension, It attempts to load the HTML template (which doesn't exist) and returns a 500 error, instead of just rendering JSON or returning a 406 error.

What could be causing this? Cheers for any help.

Tom Brunoli
  • 3,436
  • 9
  • 36
  • 54
  • Possible duplicate: http://stackoverflow.com/questions/14579774/respond-to-only-json-in-rails?rq=1 – Josh Feb 24 '14 at 03:59
  • 1
    I did see that, but I want to know why it doesn't work with just the `respond_to :json` setting – Tom Brunoli Feb 24 '14 at 04:33
  • `respond_to :json` and `respond_with @posts` properly respond with *HTTP/1.1 406 Not Acceptable* to me in Production. In Development you get a "HTTP/1.1 500 Internal Server Error" and an `ActionController::UnknownFormat` – Leonel Galán Dec 02 '15 at 16:11
  • 2
    Sorry I tried to answer but my post was deleted by a moderator since I also answered the other "possible duplicate" even though this one is more clearly Rails 4. So here you go... In Rails 4, you have to pass a lambda in the route to enforce a constraint like so: `resources :posts, constraints: lambda { |req| req.format == :json }` – Sia Jun 02 '16 at 02:23
  • You can see more in the second note in the Rails guide here: http://edgeguides.rubyonrails.org/routing.html#request-based-constraints – Sia Jun 02 '16 at 02:25
  • @TomBrunoli please check https://stackoverflow.com/questions/15227145/get-content-type-of-request/63579270#63579270 – Bhaveshkumar Aug 25 '20 at 13:20

10 Answers10

47

I believe there are 2 parts here:
1) json only requests in rails
2) json only responses in rails

1) Configure your application controller to ensure json requests only

# app/controller/application_controller.rb  
before_action :ensure_json_request  

def ensure_json_request  
  return if request.format == :json
  render :nothing => true, :status => 406  
end  

2) Configure your Rails API routes to ensure json responses only

# config/routes.rb  
MyApp::Application.routes.draw do  
  namespace :api, constraints: { format: 'json' } do  
    namespace :v1 do  
      resources :posts  
    end  
  end  
end  
csi
  • 9,018
  • 8
  • 61
  • 81
  • 4
    Instead of `params[:format] == "json" || request.headers["Accept"] =~ /json/` you can use `request.format.symbol == :json`. – ciastek Aug 24 '15 at 10:21
  • 1
    Nice call. Updated with `request.format == :json` as `symbol` is not required per this other [request format question](http://stackoverflow.com/questions/15227145/get-content-type-of-request#answer-33768811) – csi Dec 02 '15 at 15:59
  • 6
    Rather than `render :nothing => true, :status => 406` you can use `head :not_acceptable` – Eric Terry Feb 16 '16 at 16:52
42

To avoid loading the non-existent HTML template, set the default resource type as JSON in config/routes.rb:

resources :posts, :defaults => { :format => :json }
  • 5
    This does not provide a constraint, which is implied by the question. The idea would be that if .json format was not specified, rails would not match on the routes and continue on down the hierarchy. – Volte Jul 13 '15 at 23:59
22

In Rails 4, you need to pass a lambda to enforce the constraint on a route.

Unfortunately, this will NOT work because it will still try to serve up (or attempt to serve up) an html template since the format is an optional parameter:

resources :posts, constraints: { format: 'json' }

This DOES work (uses the lambda):

resources :posts, constraints: lambda { |req| req.format == :json }

See the second (final) note in this section of the Rails guide.

Sia
  • 8,894
  • 5
  • 31
  • 50
5

As you are using a before_filter, you will have a 406 Not Acceptable if a request for a format is made which is not defined.

Example:

class SomeController < ApplicationController
  respond_to :json


  def show
    @record = Record.find params[:id]

    respond_with @record
  end
end

The other way would be to add a before_filter to check for the format and react accordingly.

Example:

class ApplicationController < ActionController::Base
  before_filter :check_format


  def check_format
    render :nothing => true, :status => 406 unless params[:format] == 'json'
  end
end

But i think, you can just do it:

respond_to do |format|
  format.json { render :json => @posts }
end

Further informations: http://guides.rubyonrails.org/layouts_and_rendering.html

Franzé Jr.
  • 1,194
  • 13
  • 20
  • 1
    The problem I have with this is that the `respond_to :json` still lets html responses go through. I want all responses to be JSON, regardless of the requested format. – Tom Brunoli Feb 24 '14 at 21:55
  • Did you create your json on the view? – Franzé Jr. Oct 01 '14 at 18:40
  • `params[:format] == 'json'` is not enough. When request is made with `Accept: application/json` header, params[:format] is empty, but Rails still responds with JSON. – ciastek Aug 24 '15 at 10:22
5

You can try this, as I was also facing this issue and now it is solved by using this solution.

class PostsController < ApplicationController
  respond_to :json

  def index
    @posts = Post.all
    render json: @posts
  end
end
Shubham Abrol
  • 613
  • 6
  • 10
4

constraints was not working for POST requests and then I tried defaults it works for all.

namespace :api, :defaults => { :format => 'json' } do
    namespace :v1 do
      resources :users do
        collection do
          get 'profile'
        end
      end
      post 'signup' => 'users#create'
      post 'login' => 'user_sessions#create'
  end
end
A H K
  • 1,758
  • 17
  • 29
1

You can set it by having a before filter that sets the request to JSON explicitly.

request.format = :json

Piotr Usewicz
  • 620
  • 2
  • 6
  • 13
0

when you try a responses in json is beacuse you only need some properties i used this

@my_model=Model.select(:attributeN, :attributeN......, attributeN)
respond_to do |format|
  format.json {
    render json: @my_model
  }
end
Ezequiel García
  • 2,616
  • 19
  • 12
0

I would suggest you to try gem 'active_model_serializers'. Its really awesome and keeps clean.

ApplicationController:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception, if: Proc.new { |c| c.request.format != 'application/json' }
  protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }
end

Routes:

namespace :api, defaults: { format: :json } do
    resource :posts
end

Posts Controller:

def index
   render json: Post.all
end
7urkm3n
  • 6,054
  • 4
  • 29
  • 46
0

I'm a bit late to this but regarding your comment to this answer:

I want all responses to be JSON

The easiest solution would be:

respond_to do |format|
  format.all { render :json => @posts }
end
Lerk
  • 946
  • 1
  • 15
  • 21