Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Show Routes while Debugging RoutingError #6696

Merged
merged 3 commits into from
@schneems
Collaborator

If someone receives a routing error, they likely need to view the routes. Rather than making them visit '/rails/info/routes' or run rake routes we can give them that information on the page.

Screenshot:
Screeshot

ActionDispatch Tests Pass

@schneems
Collaborator

The only real question was where to put the code to format the routes. In a more traditional architecture it would belong in the controller 100%. However, in other exceptions there are code blocks in the views , and while it doesn't make for clean view code, it does keep the middleware lean.

@rafaelfranca

Very cool. I would leave the formatter code in the middleware.

...ck/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -1,5 +1,7 @@
require 'action_dispatch/http/request'
require 'action_dispatch/middleware/exception_wrapper'
+require 'rails/application/route_inspector'
@rafaelfranca Owner

I just realized that this will only work if railties is loaded in your application. As railties is not a actionpack dependency this can fail with a LoadError

@rafaelfranca Owner

@josevalim thoughts about this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@dmathieu
Collaborator

:+1: love the idea.

@josevalim
Owner

As @rafaelfranca, this is backwards, actionpack cannot depend on railties. In fact, every time you see a reference to the Rails constant in actionpack, it is something that needs to be fixed by providing a proper configuration or abstract (except in railties files).

That said, it seems the best way would be to move the router inspector to actionpack.

@tenderlove
Owner

Agree with @josevalim, we need to move the inspector to AP. I do like this idea, but we should be careful about performance of the 500 pages.

@josevalim
Owner
@schneems
Collaborator

ActionDispatch already knows quite a bit about routes, formatting them to be human readable doesn't seem like too far of a leap.

On the perf side it looks like DebugExceptions is in the middleware in production, but the custom error pages are ignored unless env['action_dispatch.show_detailed_exceptions'] is set. Restrict any route formatting/rendering code to within that section and there won't be any performance degradation.

@schneems
Collaborator

@josevalim, @rafaelfranca, & @tenderlove I paired with @mattt to move the Route Inspector out of Railties, and into Actionpack (action_dispatch). Re-ran all railties tests (https://gist.github.com/b298e74a2d20863c9486) and AR with sqlite3 (https://gist.github.com/b298e74a2d20863c9486). ATP.

Now action_dispatch can render routes when routing errors are received independently of railties.

@josevalim
Owner

Thanks @schneems ! One thing left: could we move the route inspector tests to inside action dispatch too? Thanks!

...ck/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -78,5 +81,13 @@ def logger(env)
def stderr_logger
@stderr_logger ||= ActiveSupport::Logger.new($stderr)
end
+
+ private
+ def formatted_routes(exception)
+ if exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error)
+ inspector = ActionDispatch::Routing::RouteInspector.new
+ inspector.format(Rails.application.routes.routes).join("\n")
@schneems Collaborator
schneems added a note

We missed this reference Rails.application.routes.routes. Moving the inspector was the easy part, getting rid of that line will require more code changes. Right now railties builds routes using ActionDispatch, but ActionDispatch doesn't actually know what routes it has. This is the method in railties for Rails.application.routes.

def routes
  @routes ||= ActionDispatch::Routing::RouteSet.new.tap do |routes|
    routes.draw_paths.concat paths["config/routes"].paths
  end

  @routes.append(&Proc.new) if block_given?
  @routes
end

My first reaction is to keep ActionDispatch agnostic about where the routes come from, but to store the results, so that Railties can inject the path where it wants the routes to be drawn from. ActionDispatch, rather than just spitting out routes can hold onto them.

def routes
  @routes ||= ActionDispatch::Routing.set_from_paths(paths["config/routes"].paths)

  @routes.append(&Proc.new) if block_given?
  @routes
end

Then we could have a getter in ActionDispatch along the lines of ActionDispatch::Routing.routes, how does that sound?

(on a side note we should also make Journey an injectable dependency rather than hard coding it into ActionDispatch, though this isn't the place or time for that)

@josevalim Owner

You can simply pass the route set or Rails.application as argument to the middleware initialization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@schneems
Collaborator

Updated the middleware to take an optional routes_app that should respond to :routes. Also moved the route inspector test to action_dispatch, which you'll be happy to know shaves off 2 seconds from the railties test suite!!

ATP on ActionPack https://gist.github.com/56da1c69f269900bb51c, ActiveRecord, and Railties

railties/lib/rails/application.rb
@@ -266,7 +266,7 @@ def default_middleware_stack
middleware.use ::ActionDispatch::RequestId
middleware.use ::Rails::Rack::Logger, config.log_tags # must come after Rack::MethodOverride to properly log overridden methods
middleware.use ::ActionDispatch::ShowExceptions, config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path)
- middleware.use ::ActionDispatch::DebugExceptions
+ middleware.use ::ActionDispatch::DebugExceptions, Rails.application
@exviva
exviva added a note

Couldn't this simply be self instead of Rails.application?

@schneems Collaborator
schneems added a note

yes, and there is a line a little lower that sets app = self, it might be more clear to move that line up and use the app var rather than relying on the hard coded

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@schneems
Collaborator

updated

@josevalim
Owner

This looks good to be merged in my opinion. It needs to be rebased though and we need a changelog entry. :) Thanks!

schneems and others added some commits
@schneems schneems show routes while debugging RoutingError
If someone receives a routing error, they likely need to view the routes. Rather than making them visit '/rails/info/routes' or run `rake routes` we can give them that information on the page.
fa714ec
@mattt mattt move route_inspector to actionpack
this is so we can show route output in the development when we get a routing error. Railties can use features of ActionDispatch, but ActionDispatch should not depend on Railties.
ef91cdd
@schneems
Collaborator

Rebased against upstream master, added changelog entry, let me know if there is anything else. Thanks for your help!

@josevalim josevalim merged commit 7404cda into rails:master
@mike-burns

Fantastic!

@klevo

So cool.

@rodrigoalvesvieira

Great idea!

@daniloassis

Simple but GREAT!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jul 7, 2012
  1. @schneems

    show routes while debugging RoutingError

    schneems authored
    If someone receives a routing error, they likely need to view the routes. Rather than making them visit '/rails/info/routes' or run `rake routes` we can give them that information on the page.
  2. @mattt @schneems

    move route_inspector to actionpack

    mattt authored schneems committed
    this is so we can show route output in the development when we get a routing error. Railties can use features of ActionDispatch, but ActionDispatch should not depend on Railties.
  3. @schneems
This page is out of date. Refresh to see the latest.
View
2  actionpack/CHANGELOG.md
@@ -1,5 +1,7 @@
## Rails 4.0.0 (unreleased) ##
+* Add routes to page while debugging a RoutingError in development. *Richard Schneeman and Mattt Thompson*
+
* Add `ActionController::Flash.add_flash_types` method to allow people to register their own flash types. e.g.:
class ApplicationController
View
19 actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -1,5 +1,7 @@
require 'action_dispatch/http/request'
require 'action_dispatch/middleware/exception_wrapper'
+require 'action_dispatch/routing/inspector'
+
module ActionDispatch
# This middleware is responsible for logging exceptions and
@@ -7,8 +9,9 @@ module ActionDispatch
class DebugExceptions
RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates')
- def initialize(app)
- @app = app
+ def initialize(app, routes_app = nil)
+ @app = app
+ @routes_app = routes_app
end
def call(env)
@@ -39,7 +42,8 @@ def render_exception(env, exception)
:exception => wrapper.exception,
:application_trace => wrapper.application_trace,
:framework_trace => wrapper.framework_trace,
- :full_trace => wrapper.full_trace
+ :full_trace => wrapper.full_trace,
+ :routes => formatted_routes(exception)
)
file = "rescues/#{wrapper.rescue_template}"
@@ -78,5 +82,14 @@ def logger(env)
def stderr_logger
@stderr_logger ||= ActiveSupport::Logger.new($stderr)
end
+
+ private
+ def formatted_routes(exception)
+ return false unless @routes_app.respond_to?(:routes)
+ if exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error)
+ inspector = ActionDispatch::Routing::RouteInspector.new
+ inspector.format(@routes_app.routes.routes).join("\n")
+ end
+ end
end
end
View
10 actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb
@@ -10,8 +10,14 @@
</ol>
</p>
<% end %>
+<%= render :template => "rescues/_trace" %>
+
+<h2>
+ Routes
+</h2>
+
<p>
- Try running <code>rake routes</code> for more information on available routes.
+ Routes match in priority from top to bottom
</p>
-<%= render :template => "rescues/_trace" %>
+<p><pre><%= @routes %></pre></p>
View
121 actionpack/lib/action_dispatch/routing/inspector.rb
@@ -0,0 +1,121 @@
+require 'delegate'
+
+module ActionDispatch
+ module Routing
+ class RouteWrapper < SimpleDelegator
+ def endpoint
+ rack_app ? rack_app.inspect : "#{controller}##{action}"
+ end
+
+ def constraints
+ requirements.except(:controller, :action)
+ end
+
+ def rack_app(app = self.app)
+ @rack_app ||= begin
+ class_name = app.class.name.to_s
+ if class_name == "ActionDispatch::Routing::Mapper::Constraints"
+ rack_app(app.app)
+ elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/
+ app
+ end
+ end
+ end
+
+ def verb
+ super.source.gsub(/[$^]/, '')
+ end
+
+ def path
+ super.spec.to_s
+ end
+
+ def name
+ super.to_s
+ end
+
+ def reqs
+ @reqs ||= begin
+ reqs = endpoint
+ reqs += " #{constraints.inspect}" unless constraints.empty?
+ reqs
+ end
+ end
+
+ def controller
+ requirements[:controller] || ':controller'
+ end
+
+ def action
+ requirements[:action] || ':action'
+ end
+
+ def internal?
+ path =~ %r{/rails/info.*|^#{Rails.application.config.assets.prefix}}
+ end
+
+ def engine?
+ rack_app && rack_app.respond_to?(:routes)
+ end
+ end
+
+ ##
+ # This class is just used for displaying route information when someone
+ # executes `rake routes`. People should not use this class.
+ class RouteInspector # :nodoc:
+ def initialize
+ @engines = Hash.new
+ end
+
+ def format(all_routes, filter = nil)
+ if filter
+ all_routes = all_routes.select{ |route| route.defaults[:controller] == filter }
+ end
+
+ routes = collect_routes(all_routes)
+
+ formatted_routes(routes) +
+ formatted_routes_for_engines
+ end
+
+ def collect_routes(routes)
+ routes = routes.collect do |route|
+ RouteWrapper.new(route)
+ end.reject do |route|
+ route.internal?
+ end.collect do |route|
+ collect_engine_routes(route)
+
+ {:name => route.name, :verb => route.verb, :path => route.path, :reqs => route.reqs }
+ end
+ end
+
+ def collect_engine_routes(route)
+ name = route.endpoint
+ return unless route.engine?
+ return if @engines[name]
+
+ routes = route.rack_app.routes
+ if routes.is_a?(ActionDispatch::Routing::RouteSet)
+ @engines[name] = collect_routes(routes.routes)
+ end
+ end
+
+ def formatted_routes_for_engines
+ @engines.map do |name, routes|
+ ["\nRoutes for #{name}:"] + formatted_routes(routes)
+ end.flatten
+ end
+
+ def formatted_routes(routes)
+ name_width = routes.map{ |r| r[:name].length }.max
+ verb_width = routes.map{ |r| r[:verb].length }.max
+ path_width = routes.map{ |r| r[:path].length }.max
+
+ routes.map do |r|
+ "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ end
+ end
+ end
+ end
+end
View
170 actionpack/test/dispatch/routing/inspector_test.rb
@@ -0,0 +1,170 @@
+require 'minitest/autorun'
+require 'action_controller'
+require 'rails/engine'
+require 'action_dispatch/routing/inspector'
+
+module ActionDispatch
+ module Routing
+ class RouteInspectTest < ActiveSupport::TestCase
+ def setup
+ @set = ActionDispatch::Routing::RouteSet.new
+ @inspector = ActionDispatch::Routing::RouteInspector.new
+ app = ActiveSupport::OrderedOptions.new
+ app.config = ActiveSupport::OrderedOptions.new
+ app.config.assets = ActiveSupport::OrderedOptions.new
+ app.config.assets.prefix = '/sprockets'
+ Rails.stubs(:application).returns(app)
+ Rails.stubs(:env).returns("development")
+ end
+
+ def draw(&block)
+ @set.draw(&block)
+ @inspector.format(@set.routes)
+ end
+
+ def test_displaying_routes_for_engines
+ engine = Class.new(Rails::Engine) do
+ def self.to_s
+ "Blog::Engine"
+ end
+ end
+ engine.routes.draw do
+ get '/cart', :to => 'cart#show'
+ end
+
+ output = draw do
+ get '/custom/assets', :to => 'custom_assets#show'
+ mount engine => "/blog", :as => "blog"
+ end
+
+ expected = [
+ "custom_assets GET /custom/assets(.:format) custom_assets#show",
+ " blog /blog Blog::Engine",
+ "\nRoutes for Blog::Engine:",
+ "cart GET /cart(.:format) cart#show"
+ ]
+ assert_equal expected, output
+ end
+
+ def test_cart_inspect
+ output = draw do
+ get '/cart', :to => 'cart#show'
+ end
+ assert_equal ["cart GET /cart(.:format) cart#show"], output
+ end
+
+ def test_inspect_shows_custom_assets
+ output = draw do
+ get '/custom/assets', :to => 'custom_assets#show'
+ end
+ assert_equal ["custom_assets GET /custom/assets(.:format) custom_assets#show"], output
+ end
+
+ def test_inspect_routes_shows_resources_route
+ output = draw do
+ resources :articles
+ end
+ expected = [
+ " articles GET /articles(.:format) articles#index",
+ " POST /articles(.:format) articles#create",
+ " new_article GET /articles/new(.:format) articles#new",
+ "edit_article GET /articles/:id/edit(.:format) articles#edit",
+ " article GET /articles/:id(.:format) articles#show",
+ " PATCH /articles/:id(.:format) articles#update",
+ " PUT /articles/:id(.:format) articles#update",
+ " DELETE /articles/:id(.:format) articles#destroy" ]
+ assert_equal expected, output
+ end
+
+ def test_inspect_routes_shows_root_route
+ output = draw do
+ root :to => 'pages#main'
+ end
+ assert_equal ["root GET / pages#main"], output
+ end
+
+ def test_inspect_routes_shows_dynamic_action_route
+ output = draw do
+ get 'api/:action' => 'api'
+ end
+ assert_equal [" GET /api/:action(.:format) api#:action"], output
+ end
+
+ def test_inspect_routes_shows_controller_and_action_only_route
+ output = draw do
+ get ':controller/:action'
+ end
+ assert_equal [" GET /:controller/:action(.:format) :controller#:action"], output
+ end
+
+ def test_inspect_routes_shows_controller_and_action_route_with_constraints
+ output = draw do
+ get ':controller(/:action(/:id))', :id => /\d+/
+ end
+ assert_equal [" GET /:controller(/:action(/:id))(.:format) :controller#:action {:id=>/\\d+/}"], output
+ end
+
+ def test_rake_routes_shows_route_with_defaults
+ output = draw do
+ get 'photos/:id' => 'photos#show', :defaults => {:format => 'jpg'}
+ end
+ assert_equal [%Q[ GET /photos/:id(.:format) photos#show {:format=>"jpg"}]], output
+ end
+
+ def test_rake_routes_shows_route_with_constraints
+ output = draw do
+ get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/
+ end
+ assert_equal [" GET /photos/:id(.:format) photos#show {:id=>/[A-Z]\\d{5}/}"], output
+ end
+
+ class RackApp
+ def self.call(env)
+ end
+ end
+
+ def test_rake_routes_shows_route_with_rack_app
+ output = draw do
+ get 'foo/:id' => RackApp, :id => /[A-Z]\d{5}/
+ end
+ assert_equal [" GET /foo/:id(.:format) #{RackApp.name} {:id=>/[A-Z]\\d{5}/}"], output
+ end
+
+ def test_rake_routes_shows_route_with_rack_app_nested_with_dynamic_constraints
+ constraint = Class.new do
+ def to_s
+ "( my custom constraint )"
+ end
+ end
+
+ output = draw do
+ scope :constraint => constraint.new do
+ mount RackApp => '/foo'
+ end
+ end
+
+ assert_equal [" /foo #{RackApp.name} {:constraint=>( my custom constraint )}"], output
+ end
+
+ def test_rake_routes_dont_show_app_mounted_in_assets_prefix
+ output = draw do
+ get '/sprockets' => RackApp
+ end
+ assert_no_match(/RackApp/, output.first)
+ assert_no_match(/\/sprockets/, output.first)
+ end
+
+ def test_redirect
+ output = draw do
+ get "/foo" => redirect("/foo/bar"), :constraints => { :subdomain => "admin" }
+ get "/bar" => redirect(path: "/foo/bar", status: 307)
+ get "/foobar" => redirect{ "/foo/bar" }
+ end
+
+ assert_equal " foo GET /foo(.:format) redirect(301, /foo/bar) {:subdomain=>\"admin\"}", output[0]
+ assert_equal " bar GET /bar(.:format) redirect(307, path: /foo/bar)", output[1]
+ assert_equal "foobar GET /foobar(.:format) redirect(301)", output[2]
+ end
+ end
+ end
+end
View
4 railties/lib/rails/application.rb
@@ -267,6 +267,7 @@ def reload_dependencies? #:nodoc:
def default_middleware_stack #:nodoc:
ActionDispatch::MiddlewareStack.new.tap do |middleware|
+ app = self
if rack_cache = config.action_controller.perform_caching && config.action_dispatch.rack_cache
require "action_dispatch/http/rack_cache"
middleware.use ::Rack::Cache, rack_cache
@@ -290,11 +291,10 @@ def default_middleware_stack #:nodoc:
middleware.use ::ActionDispatch::RequestId
middleware.use ::Rails::Rack::Logger, config.log_tags # must come after Rack::MethodOverride to properly log overridden methods
middleware.use ::ActionDispatch::ShowExceptions, config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path)
- middleware.use ::ActionDispatch::DebugExceptions
+ middleware.use ::ActionDispatch::DebugExceptions, app
middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
unless config.cache_classes
- app = self
middleware.use ::ActionDispatch::Reloader, lambda { app.reload_dependencies? }
end
View
3  railties/lib/rails/info_controller.rb
@@ -1,4 +1,4 @@
-require 'rails/application/routes_inspector'
+require 'action_dispatch/routing/inspector'
class Rails::InfoController < ActionController::Base
self.view_paths = File.join(File.dirname(__FILE__), 'templates')
@@ -16,6 +16,7 @@ def properties
def routes
inspector = Rails::Application::RoutesInspector.new
+ inspector = ActionDispatch::Routing::RouteInspector.new
@info = inspector.format(_routes.routes).join("\n")
end
View
4 railties/lib/rails/tasks/routes.rake
@@ -1,7 +1,7 @@
desc 'Print out all defined routes in match order, with names. Target specific controller with CONTROLLER=x.'
task :routes => :environment do
all_routes = Rails.application.routes.routes
- require 'rails/application/routes_inspector'
- inspector = Rails::Application::RoutesInspector.new
+ require 'action_dispatch/routing/inspector'
+ inspector = ActionDispatch::Routing::RouteInspector.new
puts inspector.format(all_routes, ENV['CONTROLLER']).join "\n"
end
Something went wrong with that request. Please try again.