Is there a way to catch all uncatched exceptions in a rails controller, like this:
def delete
schedule_id = params[:scheduleId]
begin
Schedules.delete(schedule_id)
rescue ActiveRecord::RecordNotFound
render :json => "record not found"
rescue ActiveRecord::CatchAll
#Only comes in here if nothing else catches the error
end
render :json => "ok"
end
Thank you
You can also define a rescue_from method.
class ApplicationController < ActionController::Base
rescue_from ActionController::RoutingError, :with => :error_render_method
def error_render_method
respond_to do |type|
type.xml { render :template => "errors/error_404", :status => 404 }
type.all { render :nothing => true, :status => 404 }
end
true
end
end
Depending on what your goal is, you may also want to consider NOT handling exceptions on a per-controller basis. Instead, use something like the exception_handler gem to manage responses to exceptions consistently. As a bonus, this approach will also handle exceptions that occur at the middleware layer, like request parsing or database connection errors that your application does not see. The exception_notifier gem might also be of interest.
begin
# do something dodgy
rescue ActiveRecord::RecordNotFound
# handle not found error
rescue ActiveRecord::ActiveRecordError
# handle other ActiveRecord errors
rescue # StandardError
# handle most other errors
rescue Exception
# handle everything else
raise
end
You can catch exceptions by type:
rescue_from ::ActiveRecord::RecordNotFound, with: :record_not_found
rescue_from ::NameError, with: :error_occurred
rescue_from ::ActionController::RoutingError, with: :error_occurred
# Don't resuce from Exception as it will resuce from everything as mentioned here "http://stackoverflow.com/questions/10048173/why-is-it-bad-style-to-rescue-exception-e-in-ruby" Thanks for #Thibaut Barrère for mention that
# rescue_from ::Exception, with: :error_occurred
protected
def record_not_found(exception)
render json: {error: exception.message}.to_json, status: 404
return
end
def error_occurred(exception)
render json: {error: exception.message}.to_json, status: 500
return
end
rescue with no arguments will rescue any error.
So, you'll want:
def delete
schedule_id = params[:scheduleId]
begin
Schedules.delete(schedule_id)
rescue ActiveRecord::RecordNotFound
render :json => "record not found"
rescue
#Only comes in here if nothing else catches the error
end
render :json => "ok"
end
Error handling for a nicer user experience is a very tough thing to pull off correctly.
Here I have provided a fully-complete template to make your life easier. This is better than a gem because its fully customizable to your application.
Note: You can view the latest version of this template at any time on my website: https://westonganger.com/posts/how-to-properly-implement-error-exception-handling-for-your-rails-controllers
Controller
class ApplicationController < ActiveRecord::Base
def is_admin_path?
request.path.split("/").reject{|x| x.blank?}.first == 'admin'
end
private
def send_error_report(exception, sanitized_status_number)
val = true
# if sanitized_status_number == 404
# val = false
# end
# if exception.class == ActionController::InvalidAuthenticityToken
# val = false
# end
return val
end
def get_exception_status_number(exception)
status_number = 500
error_classes_404 = [
ActiveRecord::RecordNotFound,
ActionController::RoutingError,
]
if error_classes_404.include?(exception.class)
if current_user
status_number = 500
else
status_number = 404
end
end
return status_number.to_i
end
def perform_error_redirect(exception, error_message:)
status_number = get_exception_status_number(exception)
if send_error_report(exception, status_number)
ExceptionNotifier.notify_exception(exception, data: {status: status_number})
end
### Log Error
logger.error exception
exception.backtrace.each do |line|
logger.error line
end
if Rails.env.development?
### To allow for the our development debugging tools
raise exception
end
### Handle XHR Requests
if (request.format.html? && request.xhr?)
render template: "/errors/#{status_number}.html.erb", status: status_number
return
end
if status_number == 404
if request.format.html?
if request.get?
render template: "/errors/#{status_number}.html.erb", status: status_number
return
else
redirect_to "/#{status_number}"
end
else
head status_number
end
return
end
### Determine URL
if request.referrer.present?
url = request.referrer
else
if current_user && is_admin_path? && request.path.gsub("/","") != admin_root_path.gsub("/","")
url = admin_root_path
elsif request.path != "/"
url = "/"
else
if request.format.html?
if request.get?
render template: "/errors/500.html.erb", status: 500
else
redirect_to "/500"
end
else
head 500
end
return
end
end
flash_message = error_message
### Handle Redirect Based on Request Format
if request.format.html?
redirect_to url, alert: flash_message
elsif request.format.js?
flash[:alert] = flash_message
flash.keep(:alert)
render js: "window.location = '#{url}';"
else
head status_number
end
end
rescue_from Exception do |exception|
perform_error_redirect(exception, error_message: I18n.t('errors.system.general'))
end
end
Testing
To test this in your specs you can use the following template:
feature 'Error Handling', type: :controller do
### Create anonymous controller, the anonymous controller will inherit from stated controller
controller(ApplicationController) do
def raise_500
raise Errors::InvalidBehaviour.new("foobar")
end
def raise_possible_404
raise ActiveRecord::RecordNotFound
end
end
before(:all) do
#user = User.first
#error_500 = I18n.t('errors.system.general')
#error_404 = I18n.t('errors.system.not_found')
end
after(:all) do
Rails.application.reload_routes!
end
before :each do
### draw routes required for non-CRUD actions
routes.draw do
get '/anonymous/raise_500'
get '/anonymous/raise_possible_404'
end
end
describe "General Errors" do
context "Request Format: 'html'" do
scenario 'xhr request' do
get :raise_500, format: :html, xhr: true
expect(response).to render_template('errors/500.html.erb')
end
scenario 'with referrer' do
path = "/foobar"
request.env["HTTP_REFERER"] = path
get :raise_500
expect(response).to redirect_to(path)
post :raise_500
expect(response).to redirect_to(path)
end
scenario 'admin sub page' do
sign_in #user
request.path_info = "/admin/foobar"
get :raise_500
expect(response).to redirect_to(admin_root_path)
post :raise_500
expect(response).to redirect_to(admin_root_path)
end
scenario "admin root" do
sign_in #user
request.path_info = "/admin"
get :raise_500
expect(response).to redirect_to("/")
post :raise_500
expect(response).to redirect_to("/")
end
scenario 'public sub-page' do
get :raise_500
expect(response).to redirect_to("/")
post :raise_500
expect(response).to redirect_to("/")
end
scenario 'public root' do
request.path_info = "/"
get :raise_500
expect(response).to render_template('errors/500.html.erb')
expect(response).to have_http_status(500)
post :raise_500
expect(response).to redirect_to("/500")
end
scenario '404 error' do
get :raise_possible_404
expect(response).to render_template('errors/404.html.erb')
expect(response).to have_http_status(404)
post :raise_possible_404
expect(response).to redirect_to('/404')
sign_in #user
get :raise_possible_404
expect(response).to redirect_to('/')
post :raise_possible_404
expect(response).to redirect_to('/')
end
end
context "Request Format: 'js'" do
render_views ### Enable this to actually render views if you need to validate contents
scenario 'xhr request' do
get :raise_500, format: :js, xhr: true
expect(response.body).to include("window.location = '/';")
post :raise_500, format: :js, xhr: true
expect(response.body).to include("window.location = '/';")
end
scenario 'with referrer' do
path = "/foobar"
request.env["HTTP_REFERER"] = path
get :raise_500, format: :js
expect(response.body).to include("window.location = '#{path}';")
post :raise_500, format: :js
expect(response.body).to include("window.location = '#{path}';")
end
scenario 'admin sub page' do
sign_in #user
request.path_info = "/admin/foobar"
get :raise_500, format: :js
expect(response.body).to include("window.location = '#{admin_root_path}';")
post :raise_500, format: :js
expect(response.body).to include("window.location = '#{admin_root_path}';")
end
scenario "admin root" do
sign_in #user
request.path_info = "/admin"
get :raise_500, format: :js
expect(response.body).to include("window.location = '/';")
post :raise_500, format: :js
expect(response.body).to include("window.location = '/';")
end
scenario 'public page' do
get :raise_500, format: :js
expect(response.body).to include("window.location = '/';")
post :raise_500, format: :js
expect(response.body).to include("window.location = '/';")
end
scenario 'public root' do
request.path_info = "/"
get :raise_500, format: :js
expect(response).to have_http_status(500)
post :raise_500, format: :js
expect(response).to have_http_status(500)
end
scenario '404 error' do
get :raise_possible_404, format: :js
expect(response).to have_http_status(404)
post :raise_possible_404, format: :js
expect(response).to have_http_status(404)
sign_in #user
get :raise_possible_404, format: :js
expect(response).to have_http_status(200)
expect(response.body).to include("window.location = '/';")
post :raise_possible_404, format: :js
expect(response).to have_http_status(200)
expect(response.body).to include("window.location = '/';")
end
end
context "Other Request Format" do
scenario '500 error' do
get :raise_500, format: :json
expect(response).to have_http_status(500)
post :raise_500, format: :json
expect(response).to have_http_status(500)
end
scenario '404 error' do
get :raise_possible_404, format: :json
expect(response).to have_http_status(404)
post :raise_possible_404, format: :json
expect(response).to have_http_status(404)
sign_in #user
get :raise_possible_404, format: :json
expect(response).to have_http_status(500)
post :raise_possible_404, format: :json
expect(response).to have_http_status(500)
end
end
end
end
Actually, if you really want to catch everything, you just create your own exceptions app, which let's you customize the behavior that is usually handled by the PublicExceptions middleware: https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
location in stack https://github.com/rails/rails/blob/4-2-stable/railties/lib/rails/application/default_middleware_stack.rb#L98-L99
configuring https://github.com/rails/rails/blame/4-2-stable/guides/source/configuring.md#L99
which can be as easy as using the routes http://blog.plataformatec.com.br/2012/01/my-five-favorite-hidden-features-in-rails-3-2/ or a custom controller (but see https://github.com/rails/rails/pull/17815 for reasons not to use the routes)
A bunch of the other answers share gems that do this for you, but there's really no reason you can't just look at them and do it yourself.
A caveat: make sure you never raise an exception in your exception handler. Otherwise you get an ugly FAILSAFE_RESPONSE https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L4-L22
BTW, the behavior in the controller comes from rescuable: https://github.com/rails/rails/blob/4-2-stable/activesupport/lib/active_support/rescuable.rb#L32-L51
Related
I currently have tests that ensure that certain actions are not_routable with Rspec:
it 'does not route to #create' do
expect(post: '/sectors').to_not be_routable
end
it 'does not route to #show' do
expect(get: '/sectors/1').to_not be_routable
end
But I have changed the way my exceptions are handled by using rescue_from in my ApplicationController.
Routes:
get '*unmatched_route', to: 'application#raise_not_found'
Application Controller:
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::RoutingError, with: :not_found
def not_found
respond_to do |format|
format.html { render :file => "#{Rails.root}/public/404", :layout => false, :status => :not_found }
format.xml { head :not_found }
format.any { head :not_found }
end
end
def raise_not_found
raise ActionController::RoutingError.new("No route matches #{params[:unmatched_route]}")
end
I can't quite work out though how to construct a test that checks the 404 page and its contents get rendered:
it 'does not route to #create' do
post: '/sectors'
expect(response.status).to eq(404)
expect(response).to render_template(:file => "#{Rails.root}/public/404.html")
end
I get an error on post: '/sectors', how do I simulate the post request in this case?
Use Request Specs for this.
Your test should look like:
it 'does not route to #create' do
post '/sectors'
expect(response.status).to eq(404)
expect(response).to render_template(:file => "#{Rails.root}/public/404.html")
end
Notice that post here is not a symbol, it is a method call.
Got stuck with:
' undefined method `post' for #<Class:0x000001058c0f68> (NoMethodError)'
on testing controller create action.
I'm using Rails 4, rpsec, and Factory Girl
Controller:
def create
#post = Post.new(post_params)
#post.user_id = current_user.id
if #post.save
flash[:success] = "Yay! Post created!"
redirect_to root_path
else
# flash[:error] = #post.errors.full_messages
render 'new'
end
end
Test:
describe '#create' do
post 'create', FactoryGirl.attributes_for(:post, user: #user)
response.should be_successful
end
I think post method is accessible inside it method block:
describe 'create' do
it 'should be successful' do
post :create, FactoryGirl.attributes_for(:post, user: #user)
response.should be_success
end
end
BTW I think you need to test for redirect, not success status.
Sorry for being off-topic but I just want to give you some advice.
Consider following best practices and use RSpec's expect syntax instead of should. Read more about why the should syntax is a bad idea here: http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
This is how I would rewrite your example:
describe 'create' do
it 'responds with 201' do
post :create, attributes_for(:post, user: #user)
expect(response.status).to eq(201)
end
end
In the example I'm using FactoryGirl's short syntax method attributes_for instead of FactoryGirl.attributes_for, it saves a few bytes. Here's how to make the short methods available (in spec/test_helper.rb):
RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
end
I'm testing for the status code 201 which Rails will return by default for a successful create action (redirect should be 3xx).This makes the test more specific.
Hope it's any help for writing better specs.
The issue comes from the fact that post should be used inside an it statement. I usually test my controllers like this:
describe 'POST "create"' do
let(:user) { User.new }
let(:params) { FactoryGirl.attributes_for(:post, user: user) }
let(:action) { post :create, params }
let!(:post) { Post.new }
before do
Post.should_receive(:new).and_return(post)
end
context 'on success' do
before do
post.should_receive(:save).and_return(true)
end
it 'renders success' do
action
expect(response).to be_success
end
it 'redirects' do
action
expect(response).to be_redirected
end
it 'sets flash message' do
action
expect(flash[:success]).to_not be_empty
end
end
context 'on failure' do
before do
post.should_receive(:save).and_return(false)
end
it 'renders new' do
action
expect(response).to render_template(:new)
end
end
end
My spec is like so:
describe SomeController do
before(:each) do
#request.env["HTTP_ACCEPT"] = 'application/vnd.apple.mpegurl'
end
describe 'GET #index' do
it "returns response" do
get 'index', format: :m3u8
puts response.code # prints 406
response.should be_success # fails
end
end
end
The controller:
class SomeController < AuthenticatedController
def index
Mime::Type.register "application/vnd.apple.mpegurl", :m3u8
# do some stuff
respond_to do |format|
format.m3u8 { render :m3u8 => #some_variable.html_safe }
end
end
What am I missing to get it to respond with status 200? Right now, the status returned is 406. Thanks.
Drop the #.
before(:each) do
request.env["HTTP_ACCEPT"] = 'application/vnd.apple.mpegurl'
end
I've scoured the web, but, alas, I just can't seem to get Rspec to correctly send content-type so I can test my JSON API. I'm using the RABL gem for templates, Rails 3.0.11, and Ruby 1.9.2-p180.
My curl output, which works fine (should be a 401, I know):
mrsnuggles:tmp gaahrdner$ curl -i -H "Accept: application/json" -X POST -d #bleh http://localhost:3000/applications
HTTP/1.1 403 Forbidden
Content-Type: application/json; charset=utf-8
Cache-Control: no-cache
X-Ua-Compatible: IE=Edge
X-Runtime: 0.561638
Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-02-18)
Date: Tue, 06 Mar 2012 01:10:51 GMT
Content-Length: 74
Connection: Keep-Alive
Set-Cookie: _session_id=8e8b73b5a6e5c95447aab13dafd59993; path=/; HttpOnly
{"status":"error","message":"You are not authorized to access this page."}
Sample from one of my test cases:
describe ApplicationsController do
render_views
disconnect_sunspot
let(:application) { Factory.create(:application) }
subject { application }
context "JSON" do
describe "creating a new application" do
context "when not authorized" do
before do
json = { :application => { :name => "foo", :description => "bar" } }
request.env['CONTENT_TYPE'] = 'application/json'
request.env['RAW_POST_DATA'] = json
post :create
end
it "should not allow creation of an application" do
Application.count.should == 0
end
it "should respond with a 403" do
response.status.should eq(403)
end
it "should have a status and message key in the hash" do
JSON.parse(response.body)["status"] == "error"
JSON.parse(response.body)["message"] =~ /authorized/
end
end
context "authorized" do
end
end
end
end
These tests never pass though, I always get redirected and my content-type is always text/html, regardless of how I seem to specify the type in my before block:
# nope
before do
post :create, {}, { :format => :json }
end
# nada
before do
post :create, :format => Mime::JSON
end
# nuh uh
before do
request.env['ACCEPT'] = 'application/json'
post :create, { :foo => :bar }
end
Here is the rspec output:
Failures:
1) ApplicationsController JSON creating a new application when not authorized should respond with a 403
Failure/Error: response.status.should eq(403)
expected 403
got 302
(compared using ==)
# ./spec/controllers/applications_controller_spec.rb:31:in `block (5 levels) in <top (required)>'
2) ApplicationsController JSON creating a new application when not authorized should have a status and message key in the hash
Failure/Error: JSON.parse(response.body)["status"] == "errors"
JSON::ParserError:
756: unexpected token at '<html><body>You are being redirected.</body></html>'
# ./spec/controllers/applications_controller_spec.rb:35:in `block (5 levels) in <top (required)>'
As you can see I'm getting the 302 redirect for the HTML format, even though I'm trying to specify 'application/json'.
Here is my application_controller.rb, with the rescue_from bit:
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, :with => :not_found
protect_from_forgery
helper_method :current_user
helper_method :remove_dns_record
rescue_from CanCan::AccessDenied do |exception|
flash[:alert] = exception.message
respond_to do |format|
h = { :status => "error", :message => exception.message }
format.html { redirect_to root_url }
format.json { render :json => h, :status => :forbidden }
format.xml { render :xml => h, :status => :forbidden }
end
end
private
def not_found(exception)
respond_to do |format|
h = { :status => "error", :message => exception.message }
format.html { render :file => "#{RAILS_ROOT}/public/404.html", :status => :not_found }
format.json { render :json => h, :status => :not_found }
format.xml { render :xml => h, :status => :not_found }
end
end
end
And also applications_controller.rb, specifically the 'create' action which is what I'm trying to test. It's fairly ugly at the moment because I'm using state_machine and overriding the delete method.
def create
# this needs to be cleaned up and use accepts_attributes_for
#application = Application.new(params[:application])
#environments = params[:application][:environment_ids]
#application.environment_ids<<#environments unless #environments.blank?
if params[:site_bindings] == "new"
#site = Site.new(:name => params[:application][:name])
#environments.each do |e|
#site.siteenvs << Siteenv.new(:environment_id => e)
end
end
if #site
#application.sites << #site
end
if #application.save
if #site
#site.siteenvs.each do |se|
appenv = #application.appenvs.select {|e| e.environment_id == se.environment_id }
se.appenv = appenv.first
se.save
end
end
flash[:success] = "New application created."
respond_with(#application, :location => #application)
else
render 'new'
end
# super stinky :(
#application.change_servers_on_appenvs(params[:servers]) unless params[:servers].blank?
#application.save
end
I've looked at the source code here: https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/responder.rb, and it seems it should respond correctly, as well as a number of questions on stack overflow that seem to have similar issues and possible solutions, but none work for me.
What am I doing wrong?
I realize that setting :format => :json is one solution (as noted above). However, I wanted to test the same conditions that the clients to my API would use. My clients would not be setting the :format parameter, instead they would be setting the Accept HTTP header. If you are interested in this solution, here is what I used:
# api/v1/test_controller_spec.rb
require 'spec_helper.rb'
describe Api::V1::TestController do
render_views
context "when request sets accept => application/json" do
it "should return successful response" do
request.accept = "application/json"
get :test
response.should be_success
end
end
end
Try moving the :format key inside the params hash of the request, like this:
describe ApplicationsController do
render_views
disconnect_sunspot
let(:application) { Factory.create(:application) }
subject { application }
context "JSON" do
describe "creating a new application" do
context "when not authorized" do
it "should not allow creation of an application" do
params = { :format => 'json', :application => { :name => "foo", :description => "bar" } }
post :create, params
Expect(Application.count).to eq(0)
expect(response.status).to eq(403)
expect(JSON.parse(response.body)["status"]).to eq("error")
expect(JSON.parse(response.body)["message"]).to match(/authorized/)
end
end
context "authorized" do
end
end
end
end
Let me know how it goes! thats the way I have set my tests, and they are working just fine!
Super late answer here! While you can use render_views to get the json from the controller, you can also simply write a views test:
spec/views/posts/show.json.jbuilder_spec.rb
require 'rails_helper'
RSpec.describe 'posts/show.json', type: :view do
subject(:json) { JSON.parse render }
before { assign(:post, post) }
let(:post) { Post.create title: 'New Post' }
it 'has the title attribute' do
expect(json).to include 'title' => post.title
end
end
Im trying to test a condition where on successful signup a Success Template is rendered by the following controller code
def create
#user = User.new(params[:user])
if #user.save
render :template => "success"
else
flash[:notice] = "Oops Somethings not quite right! :("
render :action => "new"
end
end
I am using the following spec to test out this code
before(:each) do
#user = User.new
#user.attributes = valid_attributes
#params = valid_attributes
#user.stub!(:save).and_return(true)
end
def do_post
post :create
end
it "should create new user " do
count = User.count
do_post
user = User.new(#params)
user.save.should eql(true)
User.count.should eql(count + 1)
end
it "should render the success page on successful signup" do
do_post
#user.save
response.should render_template("success") if #user.save
end
But the example fails "it should render success page on successful signup" with this error message
1)
'UsersController handling POST /users should render the success page on successful signup' FAILED
expected "success", got "users/new.html.erb"
./spec/controllers/users_controller_spec.rb:67:
The success view is an template stored in the views/users/ without an action. Im guessing im making a very fundamental mistake and would like some help .
You are stubbing the #user variable in the test, but the controller will instantiate a new instance so the stub won't be in place.
It's not a good idea to use a stub in this case just to emulate a successful save call. Why don't you supply valid data instead and make sure the action is successful?
The following code is for RSpec > 2.1 and it uses the expect syntax.
before(:each) do
#params = valid_attributes
end
it "should create new user" do
#_before = User.count
post :create, :user => #params
expect(assigns(:user)).to_not be_new_record
expect(User.count).to eq(#_before + 1)
end
it "should render the success page on successful signup" do
post :create, :user => #params
expect(response).to be_successful
expect(response).to render_template("success")
end
Finally, change
render :template => "success"
to
render :action => "success"
For previous RSpec versions or if you have to use the should syntax, use
before(:each) do
#params = valid_attributes
end
it "should create new user" do
#_before = User.count
post :create, :user => #params
assigns(:user).should_not be_new_record
User.count.should == (#_before + 1)
end
it "should render the success page on successful signup" do
post :create, :user => #params
response.should be_successful
response.should render_template("success")
end