I need to implement a custom error page in my rails application that allows me to use erb.
I've been following this tutorial (http://blog.tommilewski.net/2009/05/custom-error-pages-in-rails/) and I cannot get it to work locally (or remotely). I am running Rails 2.3.5
Here's the gist of the approach.
1) in the 'application_controller', I over ride the "render_optional_error_file(status_code)" method, and set the visibility to "protected", like this.
protected
def render_optional_error_file(status_code)
known_codes = ["404", "422", "500"]
status = interpret_status(status_code)
if known_codes.include?(status_code)
render :template => "/errors/#{status[0,3]}.html.erb", :status => status, :layout => 'errors.html.erb'
else
render :template => "/errors/unknown.html.erb", :status => status, :layout => 'errors.html.erb'
end
end
def local_request?
true
end
I also created a folder within views called errors and created the following views: 404.html.erb, 422.html.erb, 500.html.erb,unknown.html.erb and I created a new layout "errors.html.erb"
I can't seem to get it to work. I've been trying to trigger the 404 page by navigating to http://localhost:3000/foobar -- but, instead of getting the new 404.html.erb, I seem to be getting the standard apache 500 error. This happens when I try both mongrel_rails start and mongrel_rails start -e production.
I would suggest using exceptions to render such error pages, so you can use inheritance to group your error messages...
First, declare some (I usually do it in application_controller.rb)
class Error404 < StandardError; end
class PostNotFound < Error404; end
Then add code to ApplicationController to handle them
class ApplicationController < ActionController::Base
# ActionController::RoutingError works in Rails 2.x only.
# rescue_from ActionController::RoutingError, :with => :render_404
rescue_from Error404, :with => :render_404
rescue_from PostNotFound, :with => :render_post_not_found
def render_404
respond_to do |type|
type.html { render :template => "errors/error_404", :status => 404, :layout => 'error' }
type.all { render :nothing => true, :status => 404 }
end
true
end
def render_post_not_found
respond_to do |type|
type.html { render :template => "errors/shop_not_found", :status => 404, :layout => 'error' }
type.all { render :nothing => true, :status => 404 }
end
true
end
end
This renders errors/error_404 with the errors layout. Should get you started :)
And in your target_controller:
raise PostNotFound unless #post
Edit
Note for Rails 3
for a longer explanation on why ActionController::RoutingError doesn't work for rails 3:
Rails 3.0 Exception Handling.
Rails ticket 4444
"If your application relies on engines that extend your app with their
own routes, things will break because those routes will never get
fired!"
Firstly - have you deleted the file: 'public/500.html' If that file exists, it will override anything else that you try to do.
Secondly, using an explicit "rescue_from" in the controller (as explained in the other comment) - is a good option if you need to fine-tune the response to different kinds of errors.
You most likely get the 500 error because of an application error.
Have you checked the log files?
Update:
Are you certain that you are running 2.3.5 and not an older version that happens to be installed?
Mongrel should say which version you are running when it starts, otherwise it should say in the config/environment.rb file.
There are some errors in the code that might create the 500 error. I've changed that and also corrected a few other things I think you meant :)
def render_optional_error_file(status_code)
known_codes = ["404", "422", "500"]
status = interpret_status(status_code)
if known_codes.include?(status) # Here it should be status, not status_code (which is a symbol)
render :template => "errors/#{status[0,3]}", :status => status, :layout => 'errors' # No need to mention ".html.erb", you can do it, but it is not necessary since rails will guess the correct format.
else
render :template => "errors/unknown", :status => status, :layout => 'errors'
end
end
def local_request?
# This must return false instead of true so that the public viewer is called
# instead of the developer version.
false
end
Purpose of completeness for newer rails versions:
http://www.frick-web.com/en/blog/nifty_errorpages-gem
that is a little rails engine for handling your error pages. Maybe you will need it for newer projects. it is a good option to handle errors in my opinion.
Related
I am looking to display an error message in a jbuilder view. For instance, one route I might have might be:
/foos/:id/bars
If :id submitted by the user does not exist or is invalid, I'd like to be able to display the error message accordingly in my index.json.builder file.
Using Rails, what's the best way to get this done? The controller might have something such as:
def index
#bar = Bar.where(:foo_id => params[:id])
end
In this case, params[:id] might be nil, or that object might not exist. I'm not sure whether the best thing to do here is handle it in the controller and explicitly render an error.json.builder, or handle it in the index.json.builder view itself. What's the correct way to do this and if it's in the index.json.builder, is params[:id] available to check there? I know I can see if #bar.nil? but not sure on the inverse?
I would render index.json.builder or just inline json with :error => 'not found'
And don't forget to set proper HTTP status: :status => 404
So result could look like this:
render :json => { :error => 'not found' }, :status => 422 if #bar.nil?
I think you meant show, since index is really for lists/collections. And you should get .first on the where, otherwise you just have a relation, right? Then, use .first! to raise an error, because Rails' Rack middleware in Rails 4 public_exceptions will handle is in a basic fashion, e.g.
def show
# need to do to_s on params value if affected by security issue CVE-2013-1854
#bar = Bar.where(:foo_id => params[:id].to_s).first!
end
You can also use #bar = Bar.find(params[:id]), but that is deprecated and will be removed in Rails 4.1, after which you would have to add gem 'activerecord-deprecated_finders' to your Gemfile to use.
For index, you'd probably want #bars = Bar.all. If for some reason you want to filter and don't want to scope, etc., then you could use #bars = Bar.where(...).to_a or similar.
Rails 4: Basic Exception Handling in Rack Is Automatic
As long as the query kicks off an error, Rails 4 should be able to return the message part of the error for any supported format where to_(format) can be called on a hash (e.g. json, xml, etc.).
To see why, take a look at Rails' Rack public_exceptions middleware.
If it is html, it is going to try to read in the related file from the public directory in Rails for the status code (e.g. 500.html for a server error/HTTP 500).
If it is some other format, it will try to do to_(the format) on the hash: { :status => status, :error => exception.message }. To see how this would work go to Rails' console:
$ rails c
...
1.9.3p392 :001 > {status: 500, error: "herro shraggy!"}.to_xml
=> "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hash>\n <status type=\"integer\">500</status>\n <error>herro shraggy!</error>\n</hash>\n"
1.9.3p392 :002 > {status: 500, error: "herro shraggy!"}.to_json
=> "{\"status\":500,\"error\":\"herro shraggy!\"}"
In the middleware, you'll see the X-Cascade header in the code and in various places related to Rails' exception handling in Rack. Per this answer, the X-Cascade header is set to pass to tell Rack to try other routes to find a resource.
Rails 3.2.x: Can Handle Exceptions in Rack
In Rails 3.2.x, that code to do to_(format) for the response body, etc. is not in public_exceptions.rb. It only handles html format.
Perhaps you could try replacing the old middleware with the newer version via a patch.
If you'd rather have Rack handle your error in a more specific way without a patch, see #3 in José Valim's post, "My five favorite “hidden” features in Rails 3.2".
In that and as another answer also mentions, you can use config.exceptions_app = self.routes. Then with routes that point to a custom controller, you can handle the errors from any controller like any other request. Note the bit about config.consider_all_requests_local = false in your config/environments/development.rb.
You don't have to use routes to use exceptions_app. Although it may be a little intimidating, it is just a proc/lambda that takes a hash and returns an array whose format is: [http_status_code_number, {headers hash...}, ['the response body']]. For example, you should be able to do this in your Rails 3.2.x config to make it handle errors like Rails 4.0 (this is the latest public_exceptions middleware collapsed):
config.exceptions_app = lambda do |env|
exception = env["action_dispatch.exception"]
status = env["PATH_INFO"][1..-1]
request = ActionDispatch::Request.new(env)
content_type = request.formats.first
body = { :status => status, :error => exception.message }
format = content_type && "to_#{content_type.to_sym}"
if format && body.respond_to?(format)
formatted_body = body.public_send(format)
[status, {'Content-Type' => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}",
'Content-Length' => body.bytesize.to_s}, [formatted_body]]
else
found = false
path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale
path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path))
if found || File.exist?(path)
[status, {'Content-Type' => "text/html; charset=#{ActionDispatch::Response.default_charset}",
'Content-Length' => body.bytesize.to_s}, [File.read(path)]]
else
[404, { "X-Cascade" => "pass" }, []]
end
end
end
Note: For any problem with that handling, the failsafe implementation is in ActionDispatch::ShowExceptions here.
Rails 3 and 4: Handling Some Exceptions in Rails Controller
If you'd rather have error rendering in the controller itself, you can do:
def show
respond_with #bar = Bar.where(:foo_id => params[:id].to_s).first!
rescue ActiveRecord::RecordNotFound => e
respond_to do |format|
format.json => { :error => e.message }, :status => 404
end
end
But, you don't need to raise errors. You could also do:
def show
#bar = Bar.where(:foo_id => params[:id].to_s).first
if #bar
respond_with #bar
else
respond_to do |format|
format.json => { :error => "Couldn't find Bar with id=#{params[:id]}" }, :status => 404
end
end
end
You can also use rescue_from, e.g. in your controller, or ApplicationController, etc.:
rescue_from ActiveRecord::RecordNotFound, with: :not_found
def not_found(exception)
respond_to do |format|
format.json => { :error => e.message }, :status => 404
end
end
or:
rescue_from ActiveRecord::RecordNotFound do |exception|
respond_to do |format|
format.json => { :error => e.message }, :status => 404
end
end
Though some common errors can be handled in the controller, if you errors related to missing routes, etc. formatted in json, etc., those need to be handled in Rack middleware.
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
i was wondering if i could add an exception to route globbing in rails. in my routes.rb i have
unless params[:not_found].eql? 'admin_data'
match '*not_found', to: 'errors#error_404'
end
im trying to enforce custom error pages, except when a user visits
myapp.heroku.com/admin_data
it doesn't seem like fetching :not_found as a param works. is there a way to add an exception in routes.rb?
if it helps, in my errors_controller i have..
def error_404
#not_found_path = params[:not_found]
end
thank you
update.
i tried doing just
puts :not_found
puts %{not_found}
but doesn't seem to work either hmmm...im trying to see if i can retrieve the params from the user
It would be much more convenient to define allowed routes in routes.rb and add exception handling in application controller for routing error:
class ApplicationController < ActionController::Base
rescue_from ActionController::RoutingError, :with => :render_not_found
private
def render_not_found
render_error_page_for(404)
end
def render_error_page_for(code)
respond_to do |format|
format.html { render :file => "#{Rails.root}/public/#{code}.html", :status => code, :layout => false }
end
end
i do catch my exception handling in my application controller but unfortunately for admin_data, i don't explicitly set it in routes.rb. it gets configured somewhere in the gem with namespace or something (im not really sure)
but on a positive note... i finally fixed it! i changed my glob and did...
match '*not_found', to: 'errors#error_404', :constraints => {:subdomain => "!(admin_data.)"}
to ignore everything which uses admin_data.
I'm using Inherited Resources for my Rails 2.3 web service app.
It's a great library which is part of Rails 3.
I'm trying to figure out the best practice for outputting the result.
class Api::ItemsController < InheritedResources::Base
respond_to :xml, :json
def create
#error = nil
#error = not_authorized if !#user
#error = not_enough_data("item") if params[:item].nil?
#item = Item.new(params[:item])
#item.user_id = #user.id
if !#item.save
#error = validation_error(#item.errors)
end
if !#error.nil?
respond_with(#error)
else
respond_with(#swarm)
end
end
end
It works well when the request is successful. However, when there's any error, I get a "Template is missing" error. #error is basically a hash of message and status, e.g. {:message => "Not authorized", :status => 401}. It seems respond_with only calls to_xml or to_json with the particular model the controller is associated with.
What is an elegant way to handle this?
I want to avoid creating a template file for each action and each format (create.xml.erb and create.json.erb in this case)
Basically I want:
/create.json [POST] => {"name": "my name", "id":1} # when successful
/create.json [POST] => {"message" => "Not authorized", "status" => 401} # when not authorized
Thanks in advance.
Few things before we start:
First off. This is Ruby. You know there's an unless command. You can stop doing if !
Also, you don't have to do the double negative of if !*.nil? – Do if *.present?
You do not need to initiate a variable by making it nil. Unless you are setting it in a before_chain, which you would just be overwriting it in future calls anyway.
What you will want to do is use the render :json method. Check the API but it looks something like this:
render :json => { :success => true, :user => #user.to_json(:only => [:name]) }
authorization should be implemented as callback (before_filter), and rest of code should be removed and used as inherited. Only output should be parametrized.Too many custom code here...
In my rails app I have defined the routes so users can access records like http://mydomain.com/qwe2
But if they type a wrong "qwe2" they get a 500 page. I think a 404 would be more appropriate.
How can I change the error page that is shown? Thanks
Create a catch-all route at the bottom of config/routes.rb:
map.connect '*path', :controller => 'unknown_route'
Then in app/controllers/unknown_route_controller you can do:
class UnknownRouteController < ApplicationController
def index
render :file => "#{Rails.root}/public/404.html", :layout => false,
:status => 404
end
end
This will render your 404 page for any unknown routes.
The only reason you get a 500 code is if your application throws an exception. This could be due to a missing route, where you do not have anything defined that matches that, or because your controller or view has crashed out.
In a production environment you might want to catch all errors generated by your application and present a better error screen, if appropriate, or a 'Not Found' page if required.
For example, a brute-force catch-all exception catcher might be defined as:
class ApplicationController < ActionController::Base
if (Rails.env.production?)
rescue_from Object, :with => :uncaught_exception_rescue
end
protected
def uncaught_exception_rescue(exception)
render(:partial => 'errors/not_found', :status => :not_found)
end
end
Returning a 404-type error is easy if you can tell when you want to do it:
render(:partial => 'errors/not_found', :status => :not_found)
Make sure you have some kind of default route or you will get these errors all the time. Usually this is done by adding a catch-all route at the very end of your routes.rb:
map.default '/*path', :controller => 'default', :action => 'show'
Then you can do whatever you want with this request.