I am adding an api to an existing rails app that is not working like I want it to. I had the error below that I fixed with the fix below. I am looking for a proper explanation of why my original code was not working (was it working in older versions of rails? I'm on 4.2). I understand why the fix is working but does it have any drawbacks to the original code.
My route:
namespace :api do
namespace :v1 do
resources :users, :only => [:show]
end
end
My controller:
class Api::V1::UsersController < ApplicationController
respond_to :json
def show
respond_with User.find(params[:id])
end
end
My error:
ActionController::UnknownFormat
Processing by Api::V1::UsersController#show as HTML
Parameters: {"id"=>"1"}
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
The fix:
def show
#user = User.find(params[:id])
render :json => #user
end
My guess is that it was not clear from the request url and header parameters which format your application should return.
In your first version the request url should end with .json or the request should have a HTTP Accept header with the value application/json. Otherwise there is no way to tell that this was a request that should return JSON data.
In your second version you just say: Hey, return this as JSON, no matter what format the request has.
there was no bug you requested /api/v1/users/1 which defaults to look for html.
You could make your intention clear calling: /api/v1/users/1.json or in your routes:
namespace :api, defaults: { format: :json } do
This is mean that you make request in unsupported format (in your case not in JSON). Look at this code for more details.
I think you forget to specify Accept header or add .json suffix at the end of your URL.
Related
I am trying to create an api for my rails application in the Controllers folder I have created the following folder structure
controllers > api > v1
my routes look something like this
require 'api_constraints'
MyApp::Application.routes.draw do
devise_for :users
resources :users
......
other resources and matching for the standard application
......
# Api definition
namespace :api, defaults: { format: :json },constraints: { subdomain: 'api' }, path: '/' do
scope module: :v1,constraints: ApiConstraints.new(version: 1, default: true) do
resources :sessions, :only => [:create]
resources :users, :only => [:show]
end
end
end
I get the same error in both my sessions and users controllers. I will just post the User controller because it's shorter
class Api::V1::UsersController < ApiController
respond_to :json
def show
respond_with User.find(params[:id])
end
end
Then my tests are
require 'spec_helper'
describe Api::V1::UsersController do
describe "GET #show" do
before(:each) do
#user = FactoryGirl.create :user
get :show, id: #user.id, format: :json
end
it "returns the information about a reporter on a hash" do
user_response = JSON.parse(response.body, symbolize_names: true)
expect(user_response[:email]).to eql #user.email
end
it { should respond_with 200 }
end
end
And the output from the test is
4) Api::V1::UsersController GET #show
Failure/Error: get :show, id: #user.id, format: :json
ArgumentError:
wrong number of arguments (0 for 1)
Two problems are 1) for some reason the id isn't getting sent to the api action
2) I'm not sure how to get to the api. I thought it should be api.localhost:3000/users/1
Thanks in advance for any help
Update
This is the output for rake routes
api_sessions POST /sessions(.:format) api/v1/sessions#create {:format=>:json, :subdomain=>"api"}
api_user GET /users/:id(.:format) api/v1/users#show {:format=>:json, :subdomain=>"api"}
Update 2
This looks like a duplicate for wrong number of arguments (0 for 1) while create my user
Unfortunately the solution to this post isn't an option for me. I can't remove Devise because the User model is shared in a standard Web rails application and the api portion of the application
Update 3
I was looking at other ways of having a API with a standard app, and using devise with doorkeeper seems like a better solution than token authentication. After getting it setup I am back in the same situation of
wrong number of arguments (0 for 1)
In the server output I see the following output. This is with a valid user id.
Started GET "/api/v1/users/1" for ::1 at 2015-07-27 20:52:09 +0100
Processing by Api::V1::UsersController#show as */*
Parameters: {"id"=>"1"}
Geokit is using the domain: localhost
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
Completed 500 Internal Server Error in 21ms
ArgumentError (wrong number of arguments (0 for 1)): app/controllers/api/v1/users_controller.rb:5:in `show'
With an invalid id I get this output
Started GET "/api/v1/users/134" for ::1 at 2015-07-27 20:55:36 +0100
Processing by Api::V1::UsersController#show as */*
Parameters: {"id"=>"134"}
Geokit is using the domain: localhost
User Load (0.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 134]]
Completed 500 Internal Server Error in 6ms
NoMethodError (undefined method `api_error' for
#<Api::V1::UsersController:0x007fc5a17bf098>): app/controllers/api_controller.rb:23:in `not_found'
Update 4
After inserting some debug statements the ID is being passed through to the controller action and the user is being retrieved from the database.
The issue is with
respond_with User.find(params[:id])
rails is unable to serialize the user. I have tried replacing User with another model that does not have Devise enabled and it can serialize the model. I'm not sure why devise is causing an issue here.
1: Verify in console that FactoryGirl is able to properly return a user object created from your user factory so that isn't nil in your get request.
2: Run rake routes to verify what the API routes its generating look like. I'm assuming if you haven't previously set this up, you will need to edit your hosts file or use something like pow on mac or nginx w/ dnsmasq on Linux to enable subdomain support in your local development environment. Then manually test your API controller by whatever subdomain you configured like http://api.myappname.dev/api/v1/users/1.json to make sure you can see it returing a valid JSON response from that URL.
A little bit cleaner example of API namespacing in routes:
namespace :api, :path => "", :constraints => {:subdomain => "api"} do
namespace :v1, defaults: { format: 'json' } do
...
end
end
This took a while for me to find out the solution and you can trace what I was thinking from the post above.
In the end, I traced the issue to that a model using devise cannot be serialized like a normal model. You can't just use
respont_with User.first
or anything like that. Devise disables the default to_json method for activerecord.
A lot of solutions talked about overwriting the to_json method or using JBuilder in the model. I didn't like this because it would restrict the possible attributes you could return.
After looking at the documentation for to_json I decided that simply passing the attributes I wanted in the options was exactly what I wanted.
def show
respond_with User.find(params[:id]).as_json(only: [:id, :name])
end
After ruining one working day, I am desperate for help.
I have a Rails 4.1.0 application which also exposes a JSON API. In the API I am using devise with token_authentication. There is an Image model with paperclip attachment. While trying to download the image, the postman plugin shows success with status 200. But the image isn't downloaded.
I have tried using both the send_file and send_data method. For both the server shows two log entries. While debugging also, I can see that the 'authenticate_user_from_token' method of ApiController gets executed twice and in the second time the 'X-Auth-Token header' is missing(which is obvious because I am not sending this second request). This results in a 401 Unauthorized error and the file isn't downloaded(See logs at the bottom). I am not sure why send_file or send_data method is causing a second request to server.
Here is my code.
controllers/api/v1/images_controller.rb
class Api::V1::ImagesController < Api::V1::ApiController
def download
#image = Image.find(params[:id])
# Tried send_file
send_file #image.pic.path(:original)
# Tried send_data
data = File.read(#image.pic.path(:original))
send_data data, filename: #image.pic.original_filename, type: #image.pic.content_type
end
end
controllers/api/v1/api_controller.rb
class Api::V1::ApiController < ApplicationController
respond_to :json
before_filter :authenticate_user_from_token!
before_filter :authenticate_user!
protect_from_forgery with: :null_session
private
def authenticate_user_from_token!
user_email = params[:email].presence
user = user_email && User.find_by_email(user_email)
auth_token = request.headers["X-Auth-Token"]
if user && Devise.secure_compare(user.authentication_token, auth_token)
sign_in user, store: false
end
end
end
controllers/application_controller.rb
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
end
models/image.rb
class Image < ActiveRecord::Base
PAPERCLIP_ROOT = "#{Rails.root}/storage"
has_attached_file :pic, :styles => {:original => "720x720", :medium => "120x120", :thumbnail => "40x40"}
end
config/routes.rb
Rails.application.routes.draw do
#API
namespace :api , defaults: {format: :json} do
namespace :v1 do
resources :images do
member do
get 'download'
end
end
devise_scope :user do
post "/sign_in", :to => 'sessions#create'
post "/sign_out", :to => 'sessions#destroy'
end
end
end
end
server_log
Started GET "/api/v1/images/14/download?email=user#example.com" for 127.0.0.1 at 2014-09-21 13:57:25 +0530
ActiveRecord::SchemaMigration Load (0.7ms) SELECT "schema_migrations".* FROM "schema_migrations"
Processing by Api::V1::ImagesController#download as */*
Parameters: {"email"=>"user#example.com", "id"=>"14", "image"=>{}}
.
.
Sent file /Users/rajveershekhawat/workspace/dine_connect/storage/images/pics/000/000/014/original/images.jpg (0.5ms)
Completed 200 OK in 14067ms (ActiveRecord: 13.5ms)
Started GET "/api/v1/images/14/download?email=user#example.com" for 127.0.0.1 at 2014-09-21 13:57:40 +0530
Processing by Api::V1::ImagesController#download as HTML
Parameters: {"email"=>"user#example.com", "id"=>"14"}
User Load (0.8ms) SELECT "users".* FROM "users" WHERE "users"."email" = 'user#example.com' LIMIT 1
Completed 401 Unauthorized in 3462ms
Please help. Thanks a lot.
Update:
Can somebody tell me why there are two request logs?
Can we even download, an image over json api whithout using Base64 encoding, like a normal download?
Try using send_data instead of send_file?
You might have to do a read on the paperclip file path.
List item
My config/routes.rb file...
Rails.application.routes.draw do
namespace :api, defaults: {format: 'json'} do
namespace :v1 do
resources :hotels do
resources :rooms
end
end
end
My app/controllers/api/v1/hotels_controller.rb
module Api
module V1
class HotelsController < ApplicationController
respond_to :json
skip_before_filter :verify_authenticity_token
def index
#hotels = Hotel.all
respond_with ({hotels: #hotels}.as_json)
#respond_with(#hotels)
end
def show
#hotel = Hotel.find(params[:id])
respond_with (#hotel)
end
def create
#hotel = Hotel.new(user_params)
if #hotel.save
respond_with (#hotel) #LINE 21
end
end
private
def user_params
params.require(:hotel).permit(:name, :rating)
end
end
end
end
When I go to POST through Postman, my data saves just fine, but I get this NoMethodError. Why is this? The issue seems to be occurring at line 21, which is the respond_with(#hotel) line. Should it not just be responding with json ouput for the newly created hotel, via the show method?
(1.1ms) COMMIT
Completed 500 Internal Server Error in 76ms
NoMethodError (undefined method `hotel_url' for #<Api::V1::HotelsController:0x0000010332df58>):
app/controllers/api/v1/hotels_controller.rb:21:in `create'
Rendered /Users/.rvm/gems/ruby-2.0.0-p451#railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/_source.erb (1.0ms)
Rendered /Users/.rvm/gems/ruby-2.0.0-p451#railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb (1.7ms)
Rendered /Users/.rvm/gems/ruby-2.0.0-p451#railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb (1.4ms)
Rendered /Users/.rvm/gems/ruby-2.0.0-p451#railstutorial_rails_4_0/gems/actionpack-4.1.0/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb within rescues/layout (31.5ms)
Because your route is in the API + v1 namespace, you actually need to redirect to the api_v1_hotel_url(#hotel) after you successfully create your resource. Of course, this is an API and there is no real redirecting, but the default Rails responder doesn't know that. It also doesn't know about your routing namespaces.
With just the default responder, you would have to do
respond_with :api, :v1, #hotel
So that Rails will build a URL that exists. Alternatively, you can create a custom responder that remove the :location option. Here is the default responder: http://api.rubyonrails.org/files/actionpack/lib/action_controller/metal/responder_rb.html
Reading through the source code for that class is very helpful in understanding respond_with. For example, you don't need to use if record.save before you use respond_with with this Responder. Rails will check if the record saved successfully for you and render a 422 with errors if it failed to save.
Anyway, you can see that the responder sets up a lot of variables in it's initializer:
def initialize(controller, resources, options={})
#controller = controller
#request = #controller.request
#format = #controller.formats.first
#resource = resources.last
#resources = resources
#options = options
#action = options.delete(:action)
#default_response = options.delete(:default_response)
end
If you subclassed this responder, you could make something like this:
class CustomResponder < ActionController::Responder
def initialize(*)
super
#options[:location] = nil
end
end
You can set a controller's responder using responder=:
class AnyController < ActionController::Base
self.responder = CustomResponder
# ...
end
To be clear, let me recap:
When you use respond_with, Rails will try to infer what route to redirect to after a successful create. Imagine you had a web UI where you can create hotels. After a hotel is created, you will be redirected to that hotel's show page in the standard Rails flow. That is what Rails is trying to do here.
Rails does not understand your route namespaces when inferring the route, so it attempts hotel_url - a route which does not exist!
Adding symbols in front of the resource will allow Rails to infer the route correctly, in this case api_v1_hotel_url
In an API, you can make a custom responder which just sets the inferred location to nil, since you don't actually need to redirect anywhere with a simple JSON response. Custom responders can also be useful in many other ways. Check out the source code.
I'm trying to get a rails app running with the node.js framework batman.js via the batman-rails gem.
When I'm responding with json in my rails controller, I get a 406 error everytime, and I don't know why. Here's my controller:
respond_to :json
def index
respond_with Sample.all
end
This gives me a 406 no matter what. I do not think this is related to batman, but rails itself. But for good measure, here's my batman code:
index: (params) ->
TopNavTemplate.Sample.load (err) -> throw err if err
#set 'samples', TopNavTemplate.Sample.get('all')
And then my index.html.erb file simply says 'index', it isn't really doing anything with batman yet.
There is a lot of 406 JSON related questions, I haven't really been able to apply them yet to my situation. Is there something I'm doing wrong to make rails respond_with JSON?
Alright, I made a very simple app to check out your situation:
SamplesController.rb
class SamplesController < ApplicationController
respond_to :json
def show
respond_with Sample.find(params[:id])
end
def index
respond_with Sample.all
end
end
When I visited /samples.json and samples/1.json, it worked as intended. However, when I went to /samples and /samples/1 (no .json extension), I got a 406 error.
In order to have the URL's work without the .json extension, you need to modify your config/routes.rb file as follows:
resources :samples, defaults: {format: :json}
Otherwise, the Rails application will attempt to respond to the request with an HTML response.
I have the following controller that returns a list of tags when it receives an HTTP request to /tags
class TagsController < ApplicationController
caches_page :index
def index
respond_to do |format|
format.json {
render :json => Tag.all(:order => "name").to_json
}
end
end
end
I'm noticing that whenever a request is made to /tags, Rails is generating a cache file at /public/tags.json. However, it never seems to use this cache file. Instead, it always runs the SQL query to retrieve the tags:
Started GET "/tags" for 127.0.0.1 at 2011-06-15 08:27:29 -0700
Processing by TagsController#index as JSON
Tag Load (0.7ms) SELECT "tags".* FROM "tags" ORDER BY name
Write page <project root path>/public/tags.json (0.3ms)
Completed 200 OK in 35ms (Views: 1.1ms | ActiveRecord: 0.7ms)
Why isn't Rails using the cache file that's being generated? Is it because the request is for /tags and not /tags.json?
i think you are probably correct, you can specify the :cache_path option to tell it what to name the file so do
caches_page :index, :cache_path => '' # if not try 'tags'
you can also pass a proc, if you want to include params
caches_page :index , :cache_path => Proc.new {|controller| controller.params }
or anything else
Your cache directory could be in the wrong place. I had this issue before where I was trying to place the cache in a directory other than public.