Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    text
    copied!<h1> Update 2018 </h1> <p>Our <a href="https://github.com/richpeck/exception_handler" rel="nofollow noreferrer"><strong><code>exception_handler</code></strong></a> gem is now considered the most popular (Rails custom error pages) ↴</p> <p><a href="https://github.com/richpeck/exception_handler" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/8Pecs.jpg" alt="enter image description here"></a> <a href="https://github.com/richpeck/exception_handler" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/f2jSy.png" alt="enter image description here"></a></p> <hr> <h1>How It Works</h1> <p>All Rails exceptions are handled with <a href="http://guides.rubyonrails.org/configuring.html#rails-general-configuration" rel="nofollow noreferrer"><strong><code>config.exceptions_app</code></strong></a>. This is assigned in the <code>config/application.rb</code> or <code>config/environments/*.rb</code> files - it needs to be a callback:</p> <p><img src="https://i.stack.imgur.com/8Gmee.jpg" alt="enter image description here"></p> <p>Whenever Rails hits an error, it invokes the <a href="https://github.com/rails/rails/blob/4-0-stable/actionpack/lib/action_dispatch/middleware/show_exceptions.rb" rel="nofollow noreferrer"><strong><code>ShowExceptions</code></strong></a> middleware. This calls <code>exception_app</code> and sends the entire <code>request</code> (including <code>exception</code>) to the <code>exceptions_app</code>:</p> <p><img src="https://raw.githubusercontent.com/richpeck/exception_handler/master/readme/middleware.jpg" alt="Middleware-Powered Exceptions"></p> <p><strong><code>exceptions_app</code></strong> needs to <a href="https://github.com/rails/rails/blob/4-0-stable/actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L43" rel="nofollow noreferrer">deliver a response</a>. If not, the <strong><code>failsafe</code></strong> is loaded:</p> <pre><code> # show_exceptions.rb#L38 def render_exception(env, exception) wrapper = ExceptionWrapper.new(env, exception) status = wrapper.status_code env["action_dispatch.exception"] = wrapper.exception env["PATH_INFO"] = "/#{status}" response = @exceptions_app.call(request.env) # =&gt; exceptions_app callback response[1]["X-Cascade"] == "pass" ? pass_response(status) : response rescue Exception =&gt; failsafe_error # =&gt; raised if exceptions_app false $stderr.puts "Error during failsafe response: #{failsafe_error}\n #{failsafe_error.backtrace * "\n "}" FAILSAFE_RESPONSE end </code></pre> <p>The <strong><code>failsafe</code></strong> is stored as <a href="https://github.com/rails/rails/blob/4-0-stable/actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L18-L22" rel="nofollow noreferrer"><code>FAILSAFE_RESPONSE</code></a> at the top of <code>ShowExceptions</code>.</p> <hr> <h1>Custom Error Pages</h1> <p>If you want to create custom error pages, you need to inject your own callback into <code>config.exceptions_app</code>. This can either be done in application, or with a gem:</p> <p><img src="https://i.stack.imgur.com/sEFtR.jpg" alt="enter image description here"></p> <p>Notice how the <em><code>call</code></em> method is used - this is how a callback works. Rails (<code>env</code>) is invoked when the request is received from the Internet; when an exception is raised, <code>env</code> is passed to <code>exceptions_app</code>.</p> <p>The quality of your exception handling will be dependent on how you manage <code>env</code>. This is important; referencing <code>self.routes</code> does <em>not</em> carry the environment forward.</p> <p>The best way is to handle exceptions with a separate controller. This allows you to handle the request as if it was just another view, granting access to the <code>layout</code> and other components (<code>model</code> / <code>email</code>).</p> <p>--</p> <p>There are <em>two</em> ways to handle exceptions:</p> <ol> <li><strong>Overriding <code>404</code> / <code>500</code> routes</strong> </li> <li><strong>Invoking a controller</strong> </li> </ol> <p>Our gem was designed around our <code>controller</code> - invoked each time an <code>exception</code> is raised. This gives <em>full</em> control over the exception process, allowing for <strong>100% branded layout</strong>:</p> <p><a href="https://github.com/richpeck/exception_handler" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/O8vxa.jpg" alt="enter image description here"></a></p> <p><strong><a href="https://github.com/richpeck/exception_handler" rel="nofollow noreferrer"><code>ExceptionHandler</code></a> is now the leading production custom error pages gem for Rails.</strong></p> <p>Maintained for over 3 years, it is the simplest and most powerful exception gem for Rails. It works 100% on Rails 5 and has already been downloaded over 70,000 times.</p> <hr> <h1>Gem</h1> <p>Latest ver <a href="https://github.com/richpeck/exception_handler" rel="nofollow noreferrer"><code>0.8.0.0</code></a> has following updates:</p> <ul> <li>Custom exceptions</li> <li>Exception "mapping" (choose which exceptions to handle)</li> <li>Email notifications</li> <li>Model backend</li> <li>Sprockets 4+ integration</li> <li>RSpec Test Suite</li> <li>Locale-based views</li> </ul> <p>You can read more <a href="http://github.com/richpeck/exception_handler" rel="nofollow noreferrer">here</a>.</p> <hr> <h1>Managing Rails' Exceptions</h1> <p>If you're not interested in the gem, let me explain the process:</p> <p>All Rails exceptions are handled with the <a href="http://guides.rubyonrails.org/configuring.html#rails-general-configuration" rel="nofollow noreferrer"><strong><code>config.exceptions_app</code></strong></a> callback. This is assigned in the <code>config/application.rb</code> or <code>config/environments/*.rb</code> files - it needs to be a callback:</p> <p><img src="https://i.stack.imgur.com/KjuzX.jpg" alt="enter image description here"></p> <p>Whenever an exception is raised by your app, the <a href="https://github.com/rails/rails/blob/4-0-stable/actionpack/lib/action_dispatch/middleware/show_exceptions.rb" rel="nofollow noreferrer"><code>ShowExceptions</code></a> middleware is invoked. This middleware builds the exception into the <code>request</code> and forwards it to the <code>config.exceptions_app</code> callback.</p> <p>By default, <code>config.exceptions_app</code> points to the routes. This is why Rails comes with <code>404.html</code>, <code>500.html</code> and <code>422.html</code> in the <code>public</code> folder.</p> <p>If you want to create <em>custom</em> exception pages, you need to override the <code>config.exceptions_app</code> callback - passing the erroneous request to an appropriate handler, be it a <code>controller</code> or <code>route</code>:</p> <p>[[ middleware ]]</p> <p>The two ways to manage this effectively are either to send the erroneous requests to the routes, or to invoke a controller.</p> <p>The simplest - and most common - way is to forward the request to the routes; unfortunately, this ignores the request and prevents you from detailing the exceptions properly. </p> <p>The best way is to invoke a separate controller. This will allow you to pass the entire request, allowing you to save it, email it or do a number of other things.</p> <p>--</p> <h1>400 / 500 Errors</h1> <p><strong>Rails can <em>only</em> respond with HTTP-valid errors</strong>.</p> <p>Whilst the app's <em>exception</em> may be different, the returned status code <em>should</em> be either <code>40x</code> or <code>50x</code>. This is in line with the HTTP spec, and outlined <a href="http://guides.rubyonrails.org/layouts_and_rendering.html#the-status-option" rel="nofollow noreferrer">here</a> ↴</p> <p><img src="https://i.stack.imgur.com/5xfqA.png" alt="enter image description here"></p> <p>This means that no matter what exception handling solution you use/build, Rails <em>needs</em> to return either <code>40x</code> or <code>50x</code> errors to the browser.</p> <p>In other words, custom error pages have little to do with the <em>type</em> of exception - more how you're catching and serving the <em>browser response</em>.</p> <p>By default, Rails does this with <code>404.html</code>, <code>422.html</code> and <code>500.html</code> files in the <code>public</code> folder. If you want to handle the exception flow yourself, you need to remove these files and channel the erroneous requests to your own <code>exceptions_app</code> callback.</p> <p>This can be done with the <code>routes</code> or with a <code>controller</code> (which I'll explain now):</p> <hr> <h2>1. Routes</h2> <p>The simplest way is to let the routes handle it.</p> <p>This method is bloated and requires using multiple actions. It is also difficult to manage the responses.</p> <p><strong><a href="http://wearestac.com/blog/dynamic-error-pages-in-rails" rel="nofollow noreferrer">This tutorial</a></strong> explains:</p> <p><a href="http://wearestac.com/blog/dynamic-error-pages-in-rails" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/2fGTx.png" alt="enter image description here"></a></p> <p>This shows how to replace the <code>exceptions_app</code> with the routes directly:</p> <pre><code># config/application.rb config.exceptions_app = self.routes </code></pre> <p>Here is the code I have (Ruby 2.0.0, Rails 4.0):</p> <p><strong>Application Config</strong></p> <pre><code>#config/application.rb config.exceptions_app = self.routes </code></pre> <p><strong>Routes</strong></p> <pre><code>#config/routes.rb if Rails.env.production? get '404', to: 'application#page_not_found' get '422', to: 'application#server_error' get '500', to: 'application#server_error' end </code></pre> <p><strong>Application Controller</strong></p> <pre><code>#controllers/application_controller.rb def page_not_found respond_to do |format| format.html { render template: 'errors/not_found_error', layout: 'layouts/application', status: 404 } format.all { render nothing: true, status: 404 } end end def server_error respond_to do |format| format.html { render template: 'errors/internal_server_error', layout: 'layouts/error', status: 500 } format.all { render nothing: true, status: 500} end end </code></pre> <p><strong>Errors Layout</strong> (totally static -- for server errors only)</p> <pre><code>#views/layouts/error.html.erb &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &lt;title&gt;&lt;%= action_name.titleize %&gt; :: &lt;%= site_name %&gt;&lt;/title&gt; &lt;%= csrf_meta_tags %&gt; &lt;style&gt; body { background: #fff; font-family: Helvetica, Arial, Sans-Serif; font-size: 14px; } .error_container { display: block; margin: auto; margin: 10% auto 0 auto; width: 40%; } .error_container .error { display: block; text-align: center; } .error_container .error img { display: block; margin: 0 auto 25px auto; } .error_container .message strong { font-weight: bold; color: #f00; } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;div class="error_container"&gt; &lt;%= yield %&gt; &lt;/div&gt; &lt;/body&gt; &lt;/html&gt; </code></pre> <p><strong>Error Views</strong></p> <pre><code>#views/errors/not_found_error.html.erb &lt;div class="error"&gt; &lt;h2&gt;Sorry, this page has moved, or doesn't exist!&lt;/h2&gt; &lt;/div&gt; #views/errors/internal_server_error.html.erb &lt;div class="error"&gt; &lt;div class="message"&gt; &lt;strong&gt;Error!&lt;/strong&gt; We're sorry, but our server is experiencing problems :( &lt;/div&gt; &lt;/div&gt; </code></pre> <p>Whilst many prefer the "routes" method for its simplicity, it is neither efficient or modular. Indeed, if your application has any semblance of object orientation, you'll quickly dismiss it as a hack.</p> <p>A <em>much</em> more resounding way is to use a custom controller to catch the pure exception. This way, you can construct the flow in accordance with your application's overall structure:</p> <hr> <h2>2. Controller</h2> <p>The other option is to route all the requests to a controller. </p> <p>This is infinitely more powerful as it allows you to take the request (exception) and pass it through to the views, whilst managing it in the backend. This will allow for the likes of saving it to the database.</p> <p>This <a href="https://gist.github.com/wojtha/8433843" rel="nofollow noreferrer"><strong>gist</strong></a> shows how:</p> <p><img src="https://i.stack.imgur.com/9yY76.png" alt="enter image description here"></p> <p>It means we can hook into the middleware &amp; pass the entire <em>request</em> to a controller.</p> <p>If this controller is backed by a model and views, we can extract it into a gem (which is what we did). If you wanted to do it manually, here's how:</p> <p>--</p> <p><strong>Config</strong></p> <p>The beauty of this method is that it hooks directly into <a href="http://guides.rubyonrails.org/configuring.html#rails-general-configuration" rel="nofollow noreferrer"><code>config.exceptions_app</code></a>. This means any exception can be handled natively, allowing for more efficiency. To make sure this works, you need to put the following code into <code>config/application.rb</code> (<code>exceptions_app</code> only works in <code>production</code> - <code>development</code> shows the errors):</p> <pre><code>#config/application.rb config.exceptions_app = -&gt;(env) { ExceptionController.action(:show).call(env) } </code></pre> <p>To test, you can set the "local" requests to false:</p> <pre><code>#config/environments/development.rb config.consider_all_requests_local = false # true </code></pre> <p>--</p> <p><strong>Controller</strong></p> <p>The next step is to add an <code>exception</code> controller. Whilst this can be handled in <code>application_controller</code>, it is far better to extract into its own. Notice the call from the <code>application.rb</code> -- <code>ExceptionController.action(:show)</code>:</p> <pre><code>#app/controllers/exception_controller.rb class ExceptionController &lt; ApplicationController #Response respond_to :html, :xml, :json #Dependencies before_action :status #Layout layout :layout_status #################### # Action # #################### #Show def show respond_with status: @status end #################### # Dependencies # #################### protected #Info def status @exception = env['action_dispatch.exception'] @status = ActionDispatch::ExceptionWrapper.new(env, @exception).status_code @response = ActionDispatch::ExceptionWrapper.rescue_responses[@exception.class.name] end #Format def details @details ||= {}.tap do |h| I18n.with_options scope: [:exception, :show, @response], exception_name: @exception.class.name, exception_message: @exception.message do |i18n| h[:name] = i18n.t "#{@exception.class.name.underscore}.title", default: i18n.t(:title, default: @exception.class.name) h[:message] = i18n.t "#{@exception.class.name.underscore}.description", default: i18n.t(:description, default: @exception.message) end end end helper_method :details #################### # Layout # #################### private #Layout def layout_status @status.to_s == "404" ? "application" : "error" end end </code></pre> <p>--</p> <p><strong>Views</strong></p> <p>There are two views to add to get this working.</p> <p>The first is the <code>exception/show</code> view, and second is the <code>layouts/error</code>. The first is to give the <code>exception_contoller#show</code> a view, and the second for <code>500</code> internal server errors.</p> <pre><code>#app/views/exception/show.html.erb &lt;h1&gt;&lt;%= details[:name] %&gt;&lt;/h1&gt; &lt;p&gt;&lt;%= details[:message] %&gt;&lt;/p&gt; #app/views/layouts/error.html.erb (for 500 internal server errors) &lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"&gt; &lt;html&gt; &lt;head&gt; &lt;title&gt;Error&lt;/title&gt; &lt;style&gt; html { height: 100%; background: #fff; } body { font-family: Helvetica, Arial, Sans-Serif; font-size: 14px; } .error_container { display: block; margin: auto; margin: 10% auto 0 auto; width: 40%; } .error_container .error { display: block; text-align: center; } .error_container .error img { display: block; margin: 0 auto 15px auto; } .error_container .message &gt; * { display: block; } .error_container .message strong { font-weight: bold; color: #f00; } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;div class="error_container"&gt;&lt;%= yield %&gt;&lt;/div&gt; &lt;/body&gt; &lt;/html&gt; </code></pre> <hr> <h1>Conclusion</h1> <p>The <em>exception</em> doesn't matter as much as the <em>error code</em>. </p> <p>When Rails raises an exception, it assigns one of the above <code>HTTP</code> response codes. These allow your browser to determine whether the request was successful.</p> <p>When dealing with exceptions, you need to ensure you're able to handle <code>40*</code> errors (which will typically use the same layout as the rest of your app) and the <code>50*</code> errors (which will need their own layout). </p> <p>In both cases, you'll be best using a separate <code>exception</code> controller, which will allow you to manage the <code>exception</code> as an object.</p>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload