Skip to content

Instantly share code, notes, and snippets.

@eprothro
Last active December 10, 2021 16:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eprothro/58b6b0a4cc02773b1ca84468f5a51f6f to your computer and use it in GitHub Desktop.
Save eprothro/58b6b0a4cc02773b1ca84468f5a51f6f to your computer and use it in GitHub Desktop.

Use a glob route at the bottom of your scopes to catch invalid routes

# config/routes.rb
Rails.application.routes.draw do
  # ... all your scopes and routes
  match "(*any)", to: "application#render_not_found", via: :all
end

You can route unmatched routes individually within constrained contexts if desired

# config/routes.rb
Rails.application.routes.draw do
  constraints subdomain: 'www' do
    scope module: :web, as :web do
      # .... routes
      # www.example.com/bad_path routes to web/base_controller
      match "(*any)", to: "base#render_not_found", via: :all
    end
  end
  constraints subdomain: 'admin' do
    scope module: :web, as :web do
      # .... routes
      # admin.example.com/bad_path routes to admin/base_controller
      match "(*any)", to: "base#render_not_found", via: :all
    end
  end
  
  # bad_subdomain.example.com/bad_path routes to application_conroller
  match "(*any)", to: "application#render_not_found", via: :all
end

Rescue From exceptions at the highest level

Desclare rescue_froms in ApplicationController, delegating to methods (via symbols).

That way rescue definitions will be inhereted by all our controllers, so they get exception handling for free but can override if desired by overriding the methods.

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :exception

  unless Rails.env.development?
    # NOTE: these are rescued from bottom (most specific) to top (least specific)
    rescue_from Exception, with: :render_exception
    rescue_from Timeout::Error, with: :render_service_unavailable
    rescue_from Rack::Timeout::Error, with: :render_service_unavailable
    rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
    rescue_from ActionController::ParameterMissing, with: :render_bad_request
  end

  protected

  def render_exception(exception, opts = {})
    opts[:template] ||= "errors/internal_server_error"
    opts[:status]   ||= :internal_server_error

    render_error(opts.merge(exception: exception))
  end

  def render_bad_request
    render_error template: "errors/bad_request", status: :bad_request # 400
  end

  def render_forbidden
    render_error template: "errors/forbidden", status: :forbidden # 403
  end

  def render_not_found
    render_error template: "errors/not_found", status: :not_found # 404
  end

  def render_service_unavailable(exception, opts = {})
    opts[:template] ||= "errors/service_unavailable"
    opts[:status]   ||= :service_unavailable

    # The timeout may have occured during a
    # persistence layer query; resetting the connection
    # cancels the query so that a "closed connection" exception
    # will not be raised during the next request
    # that executes a query
    ActiveRecord::Base.connection.reset!

    render_error(opts.merge(exception: exception))
  end

  def render_unauthorized
    render_error template: "errors/unauthorized", status: :unauthorized # 401
  end

  def render_error(opts = {})
    # ErrorReporter.report(opts[:exception], request: request) if opts[:exception].present?

    render opts[:template], status: opts[:status]
  end
end

These methods render default or "fallback" views

# views/errors/not_found.html.haml
.error
  .error-container
    %h3 This page cannot be found!

    %p
      = link_to "Click here", root_path
      to return to the home page.

Different contexts can then style error pages as they see fit

# sytlesheets/web/_errors.scss
.error {
  max-width: $site-width;
  margin: 5% auto;

  > .error-container {
    text-align: center;
  }
}
# sytlesheets/admin/_errors.scss
.error {
  max-width: $site-width;
  margin: 5% auto;

  > .error-container {
      font-weight: bold;
  }
}

If styling isn't enough and specific markup is needed for a context, they can override the method

module Web
  class BaseController < ApplicationController
    private
    
      def render_not_found
        render_error template: "web/errors/not_found", status: :not_found # 404
      end
  end
end
# views/web/errors/not_found
%h2 404 - Here's a specific error message
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment