How to render file in Rails 5 API? - ruby-on-rails

I have a single-page application written in React with Ruby on Rails back-end (API mode). Rails is also serving static files. I'm pointing Rails router to public/index.html, so my SPA could manage his own routing with react-router. This is common practice in order to make direct links and refresh to work.
routes.rb
match '*all', to: 'application#index', via: [:get]
application_controller.rb
class ApplicationController < ActionController::API
def index
render file: 'public/index.html'
end
end
The problem is this doesn't work in API mode. It's just an empty response. If I change the parent class to ActionController::Base everything works as expected. But I don't want to inherit the bloat of full class, I need slim API version.
I've tried adding modules like ActionController::Renderers::All and AbstractController::Rendering without success.

If I change the parent class to ActionController::Base everything works as expected. But I don't want to inherit the bloat of full class, I need slim API version.
Yes, if you serve index from ApplicationController, changing its base class would affect all other controllers. This is not good. But what if you had a specialized controller to serve this page?
class StaticPagesController < ActionController::Base
def index
render file: 'public/index.html'
end
end
This way, you have only one "bloated" controller and the others remain slim and fast.

You could do
render text: File.read(Rails.root.join('public', 'index.html')), layout: false

I usually just redirect_to 'file path'.
def export
# When the route coming to get 'some_report/export', to: 'greate_controller#export'
# The file where you write or preparing, then you can redirect some path like : http://localhost:3000/public/tmpfile/report20210507.xlsx
# And it will just redirect the file for you
file_path = "public/tmpfile/report20210507.xlsx"
redirect_to "#{root_url}#{file_path}"
end
For this example
root_url = "http://localhost:3000/"

This should work, and allow you to keep inheriting from ActionController::API--
class ApplicationController < ActionController::API
def index
respond_to do |format|
format.html { render body: Rails.root.join('public/index.html').read }
end
end
end
The render logic changed for ActionController::API with Rails 5.

Related

How to access nested controllers in rails?

I have a web application and I want to extend it to a Web API. I have split my controller into 2 folders Web, and Api. I have my ApplicationController in the Web folder and want to have a ApiController in the api folder.
I know that the ActionController is only supposed to be inherited from the ApplicationController. Is there a way that I can use 2 separate controllers so I can have different behaviour in each?
An example why I want 2 is so I can handle CanCan exceptions in different ways and protect_from_forgery with differently.
Update
When I try to run my tests I get the following error
api_sessions_controller.rb:1:in `': uninitialized constant ApiController (NameError)
My Api Sessions Controller is
class ApiSessionsController < ApiController
def create
user_password = params[:session][:password]
user_email = params[:session][:email]
user = user_email.present? && User.find_by(email: user_email)
if user.valid_password? user_password
sign_in user, store: false
user.generate_authentication_token!
user.save
render json: user, status: 200, location: [:api, user]
else
render json: { errors: "Invalid email or password" }, status: 422
end
end
end
And the Api Controller is
class ApiController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :null_session
# include SessionsHelper
rescue_from CanCan::AccessDenied do |exception|
if user_signed_in?
flash[:error] = "Access denied!"
redirect_to root_url
else
flash[:error] = "Please Sign in"
redirect_to new_user_session_path
end
end
end
I'm at a loss as to what is causing this, I can't see what is wrong with either controller and this is why I thought there might have been some conflict with what I was doing and the rails framework
I agree with ABMagil, nobody says you HAVE to inherit ApplicationController. In your case, you especially mention you want different access control + CSRF protections, so it's totaly justified to have another application controller that inherits from ActionControlle::Base
Anyway, everything will happen in your routes :
namespace :api do
# Stuff here will automatically look inside controllers/api/ folder,
# URL prefixed with /api/
# View url/path helpers with prefix api_
end
# your_domain.com/api/my_resource/
scope module: 'web' do
# Stuff here will look for controllers in controllers/web/
# NO URL prefix
# NO url/path prefix for View helpers
end
# your_domain.com/my_resource
EDIT : concerning your error
When namespacing, it means you are wrapping your classes in a module, so you have to specify this for every sub-controller. i.e
You can either have ActionController and ApiController in the /controller/ folder (I suggest you to do this + 3rd bullet point, feels more clear)
controllers/api_controller.rb,
controllers/application_controller.rb
class ApiController, class ApplicationController
If you want to move them to api/web subfolders, you have to reflect that in the files
controllers/api/api_controller.rb,
controllers/web/application_controller.rb
class Api::ApiController, class Web::ApplicationController
And obviously, also respect this for your other controllers
controllers/api/session_controller.rb
class Api::SessionController
Also note, if you want to keep API (only capital letters), I think it works when you use folder name /API/
Who says only ApplicationController can/should inherit from ActionController? ApplicationController sets the path to view files (not relevant for APIs) and defines require_local! and local_request?. If you don't use those methods (require_local! is called by InfoController and MailersController, within the Rails codebase), then you're not losing anything.
If you have logic that's only for your API, you certainly could subclass ActionController directly, but is it necessary? If you subclass ApplicationController but override relevant methods, won't that cover your use case?

Rabl Api and preventing code duplication

I am working on a simple API for a rails project which should also be able for versioning.
I am using rabl-rails gem.
To prevent code duplication, I wanna be able to use my ControllerActions (ex. UserController#search) twice. One time for the normal WebUsers, and one for the API.
I only saw people writing controllers like this:
class Api::v1::UsersController
def list
#user = User.all
end
end
Do I have to Namespace Controllers for RABL?
Or is it possible to "route or delegate" my JSON requests to existing Controllers?
For example my regular UsersController Action "list" has actually this:
def list
respond_to do |format|
format.html
format.json { render #user }
end
end
views/users/list.json.rabl also exist and works well in this case.
Now I try to move the rabl files into
/api/v1/users/list.json.rabl
I provide a Route:
namespace :api, :defaults => {:format => :json} do
namespace :v1 do
match 'users/list', to: 'Users#list', as: 'users', via: [:get, :post]
end
end
At the moment I do not provide a Api::V1::UsersController.
What is the best approach to
provide a /api/v1/users/list route but
use the regular UsersController and
have the list.json.rabl view in the /api/v1/ folder?
I hope it's not to complicated explained...
I ended up with the idea, that an API should have his own logic. Some actions have view specific stuff (breadcrumbs, dropdown values, etc.) inside.
With my knowledge "search logic" can be extracted into a module which gets mixed into your ApplicationController class (inlcude). Then you can use this logic in both places: Api and regular web view.
My first attempt is here:
BaseController
# Base Controller for all Api requests
class Api::V1::BaseController < ApplicationController
respond_to :json
acts_as_token_authentication_handler_for User
end
Api SubControllers
class Api::V1::UsersController < Api::V1::BaseController
# Inherit from BaseController -------^^^^^^^^^^^^^^^^^^^^^^^
# your personal extracted logic
include SearchLogic
def list
#user = User.all
end
end
So

How can I customize the way Rails 4 render() finds files?

I'm serving a versioned web service from Rails.
I would very much like to be able to call render like normal:
render 'index'
And have it correctly serve the requested version from the following:
index.v1.json.jbuilder
index.v2.json.jbuilder
index.v3.json.jbuilder
Assuming that I already know the requested version within the context of the controller action execution, how do I get render() to leverage it?
I have used the versioncake gem
You should definitely check this out. File name will be very close to what you have:
index.v1.json.jbuilder
would be
index.json.v1.jbuilder
Sounds like a builder design pattern might work here. Have a view builder object that returns the desired behavior.
module ViewBuiler
def build(name, api_version)
View.new(name, api_version).build
end
class View < Struct(:name, :api_version)
def build
[name, api_version, 'json', 'jbuilder'].join('.')
end
end
end
and in your controller you could just do something like:
ApplicationController
include ViewBuilder
end
MyController < ApplicationController
def index
...
# you can pass either strings or symbols to build and it will
# return 'index.v1.json.jbuilder'
render build(:index, params[:api_version])
end
end
And disclaimer, this is just an idea and not something I've implemented. I like the approach for 2 reason. The Controller actions remain skinny and don't have logic in it. A builder like this seems pretty easy to test. And it keeps the thing that might change (views etc) isolated into something that can change frequently as long as it retains it's interface that the Controllers will work with.
This seems like a candidate for Variants. This is new to 4.1 though.
class MyController < ActionController::Base
before_action :set_variant
def my_action
.....
respond_to do |format|
format.json do |json|
json.v1 do
# render whatever you need here
end
end
end
end
protected
def set_variant
request.variant = :v1 if request.params[:version] == "v1"
....
end
end

UnknownFormat in Devise::SessionsController#new

I have a Rails 4 app (that was upgraded from Rails 3) in which I decided to delete one of the controllers. I then moved the methods from that deleted controller to the ApplicationController, which included before_filter :authenticate_user!
Here's what my ApplicationController looks like now:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :authenticate_user!
respond_to :json
def index
gon.rabl
#user = current_user
gon.rabl "app/views/users/show.json.rabl", as: "current_user"
end
def markdown
require 'redcarpet'
renderer = Redcarpet::Render::HTML.new
extensions = {}
Redcarpet::Markdown.new(renderer, extensions)
end
helper_method :markdown
end
Now, I'm getting this error:
ActionController::UnknownFormat in Devise::SessionsController#new
I think this might be due to the fact that you have set your application controller to respond only to json. If your Devise Controller inherits from ApplicationController (I think this is the default), then it will expect to see a content-type: json header, or your urls must all end in .json
You shouldn't have the index method defined in application_controller. You should move it to the appropriate controller. If this is something you want to do before every action you might want to try something like this:
before_action :gon_user, only: :index
private
def gon_user
gon.rabl
#user = current_user
gon.rabl "app/views/users/show.json.rabl", as: "current_user"
end
Though i've to be honest that i'm not sure about the gon stuff, can't remember if it was for moving data from ruby to javascript or for responding to ajax/json request.
Thanks Slicedpan. Got me thinking about a
respond_to :json
Used in my Rails Application as an API with Angular. As in my Rails controllers I use for requests from my Angular Services.
respond_with
In my case I ended up adding html to the respond_to:
respond_to :json, :html
The Default Mime Types can be seen here:
http://apidock.com/rails/Mime

Rails3 filter is being skipped in production

I have a strange behavior of Rails 3.0.10. Got this application controller:
app/controllers/application_controller.rb
require 'api/api_controller'
# rest not important
app/controllers/api/api_controller.rb
class Api::ApiController < ActionController::Base
before_filter :require_user
def require_user
#user = User.find(xxx, yyy)
end
end
and then this controller:
app/controllers/api/ac_controller.rb
class Api::AcController < Api::ApiController
before_filter :find_pool, :only => [:add_pool, :remove_pool]
def add_pool
# some logic that needs #user to be set
end
def remove_pool
# some logic that needs #user to be set
end
def find_pool
# some logic here
end
end
My problem is when I run this in production mode the require_user filter is NOT called. When I try this in the development mode, it works.
Now, I understand in development mode classes are being reloaded, but the question is why the require_user filter does NOT get called?
Edit: Please note AC controller is before API controller lexicographically.
It looks like order of required files problem or the ApiController being loaded twice. Once before AcController and one more time after AcController being loaded. This could cause, that find_pool filter will get evaluated before require_user. ApiController is alse after AcController in lex order.
The problem might be caused by require "api_controller" being present somewhere - it should be handled by Rails and does not need to be put down explicitly. So if there is such a line, removing it could help.
Typically the methods called by filters should NOT be public. Public methods in controllers are treated as actions. Have you tried making require_user a private method?

Resources