So I handle exceptions with an error controller to display dynamic content to my users in production. I have it in my route file to do:
# Errors
%w( 404 422 500 ).each do |code|
get code, :to => "errors#show", :code => code
end
The only problem is now that I'm routing on errors such as that I lose information in my controller when I want to notify Airbrake. How can I maintain the exception information and send it to Airbrake on a 500? Right now all I get is the env that was occurring at the time of the exception which is less helpful for debugging purposes.
class ErrorsController < ApplicationController
def show
notify_airbrake(env)
render status_code.to_s, :status => status_code
end
protected
def status_code
params[:code] || 500
end
end
Are you handling an error by redirecting to a URL like http://your.site/500? That will be just an HTTP request like any other, losing the exception context you're after. Instead, you probably want to be using ActionController's Rescue functionality. It would look like this:
class ApplicationController < ActionController::Base
rescue_from StandardError, with: :render_error
private
def render_error(error)
notify_airbrake(error)
render text: 500, status: 500
end
end
You can add multiple rescue_from declarations to handle different kinds of error, like the ActiveRecord::RecordNotFound from the Rails guide's example.
Related
I have a Rails web app with some forms users can use to comment etc. They are frequently being used by spammers trying to create spam comments. Nothing new about that.
Sometimes I get CSRF-hack attempts which causes an ActionController::InvalidAuthenticityToken exception. This happens quite a lot and I would like to rescue and send the user/bot to a "You failed to comment"-page.
This has turned out to be a tricky exception to catch though since I can't recreate the error myself. I first put a rescue in the #create when the model (created by the form) was to be saved but it doesn't catch the exception. In order to do so I consider having it cover the entire controller section but that seems over the top.
My two questions:
Is there a way to re-create an ActionController::InvalidAuthenticityToken error by myself, so I can test it?
When is the exception raised? During .save? On Comment.new? Basically, where should I put my begin/rescue?
Thanks!
You can rescue from this exception in your controller. One way you can set this up is to add a rescue in your ApplicationController.
class ApplicationController < ActionController::Base
rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_auth_token
private
def record_not_found
render :text => "You failed to comment", :status => 422
end
end
You could also just capture the exception locally in your controller action.
def create
begin
# Create your comment
rescue ActionController::InvalidAuthenticityToken
# Render your last view with some error text.
end
end
You can add raise ActionController::InvalidAuthenticityToken inside your action if you want to test it out.
I am writing an application that uses plain old Ruby objects (POROs) to abstract authorization logic out of controllers.
Currently, I have a custom exception class called NotAuthorized that I rescue_from at the controller level, but I was curious to know: Does Rails 4 already come with an exception to indicate that an action was not authorized? Am I reinventing the wheel by implementing this exception?
Clarification: The raise AuthorizationException is not happening anywhere inside of a controller, it is happening inside of a completely decoupled PORO outside of the controller. The object has no knowledge of HTTP, routes or controllers.
Rails doesn't seem to map an exception to :unauthorized.
The default mappings are defined in activerecord/lib/active_record/railtie.rb:
config.action_dispatch.rescue_responses.merge!(
'ActiveRecord::RecordNotFound' => :not_found,
'ActiveRecord::StaleObjectError' => :conflict,
'ActiveRecord::RecordInvalid' => :unprocessable_entity,
'ActiveRecord::RecordNotSaved' => :unprocessable_entity
)
and actionpack/lib/action_dispatch/middleware/exception_wrapper.rb:
##rescue_responses.merge!(
'ActionController::RoutingError' => :not_found,
'AbstractController::ActionNotFound' => :not_found,
'ActionController::MethodNotAllowed' => :method_not_allowed,
'ActionController::UnknownHttpMethod' => :method_not_allowed,
'ActionController::NotImplemented' => :not_implemented,
'ActionController::UnknownFormat' => :not_acceptable,
'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
'ActionDispatch::ParamsParser::ParseError' => :bad_request,
'ActionController::BadRequest' => :bad_request,
'ActionController::ParameterMissing' => :bad_request
)
You could add a custom exception from within your application's configuration (or a custom Railtie):
Your::Application.configure do
config.action_dispatch.rescue_responses.merge!(
'AuthorizationException' => :unauthorized
)
# ...
end
Or simply use rescue_from.
I'm guessing the reason Rails didn't introduce this exception is because Authorisation and Authentication is not Rails native behavior (not considering basicauth of course).
Usually these are responsibilities of other libraries Devise for NotAuthenticated; Pundit, Dude Policy, CanCanCan, Rollify for NotAuthorized) I would actually argue it may be a bad thing to extend ActionController with custom exceptions like ActionController::NotAuthorized (because like I said it's not it's responsibility)
So Way how I usually tackled this problem is that I've introduced custom exceptions on ApplicationController
class ApplicationController < ActionController::Base
NotAuthorized = Class.new(StandardError)
# ...or if you really want it to be ActionController
# NotAuthorized = Class.new(ActionController::RoutingError)
rescue_from ActiveRecord::RecordNotFound do |exception|
render_error_page(status: 404, text: 'Not found')
end
rescue_from ApplicationController::NotAuthorized do |exception|
render_error_page(status: 403, text: 'Forbidden')
end
private
def render_error_page(status:, text:, template: 'errors/routing')
respond_to do |format|
format.json { render json: {errors: [message: "#{status} #{text}"]}, status: status }
format.html { render template: template, status: status, layout: false }
format.any { head status }
end
end
end
Therefore in my controllers I can do
class MyStuff < ApplicationController
def index
if current_user.admin?
# ....
else
raise ApplicationController::NotAuthorized
end
end
end
This clearly defines that the layer your expecting this exception to be raised and caught is your application layer, not 3rd party lib.
The thing is that libraries can change (and yes this means Rails too) defining exception on a 3rd party lib classes and rescuing them in your application layer is really dangerous as if the meaning of exception class changes it brakes your rescue_from
You can read lot of articles where people are Waring about Rails raise - rescue_from being the modern goto (now considering anti-pattern amongst some experts) and in certain extend it is true, but only if you are rescuing Exceptions that you don't have full control off !!
That means 3rd party exceptions (including Devise and Rails to certain point). If you define the exceptions classes in your application, you are not relaying on 3rd party lib => you have full control => you can rescue_from without this being an anti-pattern.
I am using rails-api to build an API with no web interface. When I get errors in development, I'd love to see just the error message and stacktrace in plain text without all of the HTML wrapping. How do I override the global exception handling so it renders the stacktrace in development mode in plain text/JSON, and a generic error message in production?
I would suggest that including the stack trace in production code is probably not a good idea from a security stand point.
Here is how I would do it:
render :json => {message:exception.message, stack_trace: exception.stacktrace}
I hope this helps.
After Sam's clarification I can add:
In your base controller for your API (probably ApplicationController):
class ApplicationController < ActionController::Base
...
rescue_from Exception do |exception|
error = {message:exception.message}
error[:stack_trace] = exception.stacktrace if Rails.env.development?
render :json => error
end
...
end
Caveat: You may not want to rescue from every single exception in this way but this is how you'd do it if you did.
Some improvements over #donleyp answer to get a clean trace in Rails 3.2 and output generic error info in production:
class ApplicationController < ActionController::API
...
rescue_from Exception do |exception|
if Rails.env.development?
error = {message:exception.message}
error[:application_trace] = Rails.backtrace_cleaner.clean(exception.backtrace)
error[:full_trace] = exception.backtrace
render :json => error
else
render :text => "Internal server error.", :status => 500
end
end
...
end
I'm running a Rails 4.0.0.rc application using New Relic for availability / exception monitoring. I modified application.rb with this snippet to enable dynamic exception pages:
config.exceptions_app = self.routes
However, I no longer see 404, 422 or 500 exceptions in New Relic. Any idea how I get them back?
Edit:
Note: this is what the controller handling the status looks like:
class ErrorsController < ApplicationController
# GET /404
def missing
render status: 404
end
# GET /422
def unprocessable
render status: 422
end
# GET /500
def exception
render status: 500
end
end
It sounds like you want to call NewRelic::Agent.notice_error manually.
You can reconstruct the request object from the Rack env and build an exception however you would like.
Something like this:
request = Rack::Request(env)
options = {
:uri => request.url,
:referrer => request.referrer,
:request_params => request.params
}
NewRelic::Agent.notice_error(your_custom_exception, options)
Note that the request parameters will be transmitted as is so be careful to filter anything sensitive.
Sources:
I work for New Relic as Ruby Agent Engineer
Documentation for NoticedError: http://rubydoc.info/gems/newrelic_rpm/frames
You will have to set the html status code to the correct value in your errors controller. If you for example have something like this:
class ErrorsController < ApplicationController
# 404
def not_found
render "not_found", status: 404
end
end
Otherwise will rails render the error page with a 200 status code, and new relic will not pick it up as an error.
I have one rails application in which I have two sections, so I want to use two different layouts for the Error page.
For example, if an error is coming from Section 1 then layout1 / different page should be used for the Error (404, 500).
If error is coming from Section 2 then layout2 / different page should be used for the Error (404, 500).
I've written code to define the Error page, enabled with erb and ruby code.
in application.rb
config.exceptions_app = self.routes
in routes.rb
match "/404", :to => "errors#error_404"
match "/500", :to => "errors#error_500"
Updated
Thought about it a little. If you only have a few types of errors, how about doing it like this?
In your routes.rb, at the very last line, add a
match '/my_segment/*path', :to => 'errors#not_found'
This should match any path that is not defined (which normally throws ActionController::RoutingError) and push it to your global error page.
You can play with play with the segments wildcard above to get your correct path. This should NOT affect your predefined paths, like mydomain.com/controller1.
Below is a more fine grained method of control.
This will help you match any errors from mydomain.com/some_controller/bad_params
def firstController < ApplicationController
def method_in_first_controller
# Do something here
rescue
#error = # Error object here
render :template=>"some_error_template", :status => :not_found # In specific action
end
end
def secondController < ApplicationController
rescue_from ActiveRecord::RecordNotFound, :with => :rescue_not_found # In secondController
def method_in_second_controller
# Do something
end
protected
def rescue_not_found
#error = # Error object here
render :template => 'some_error_template', :status => :not_found
end
end
def ApplicationController
rescue_from ActiveRecord::RecordNotFound, :with => :rescue_not_found # Globally
protected
def rescue_not_found
#error = # Error object here
render :template => 'application/not_found', :status => :not_found
end
end
Using referrer doesn't seem to get anywhere, sorry for the bad answer yesterday.
In your errors controller you can have a check who is the referrer and have a conditional layout based on that