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

11 participants

Richard Schneeman Rafael Mendonça França Damien Mathieu José Valim Aaron Patterson Robert Starsi Rodrigo Alves Danilo Assis Olek Janiszewski Mike Burns Mattt Thompson
Richard Schneeman
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

Richard Schneeman
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.

Rafael Mendonça França

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'
Rafael Mendonça França 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

Rafael Mendonça França Owner

@josevalim thoughts about this?

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

:+1: love the idea.

José Valim
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.

Aaron Patterson
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.

José Valim
Owner
Richard Schneeman
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.

Richard Schneeman
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.

José Valim
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")
Richard Schneeman 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)

José Valim 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
Richard Schneeman
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
Olek Janiszewski
exviva added a note

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

Richard Schneeman 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
Richard Schneeman
Collaborator

updated

José Valim
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
Richard Schneeman 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 Thompson 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
Richard Schneeman
Collaborator

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

José Valim josevalim merged commit 7404cda into from
Mike Burns

Fantastic!

Robert Starsi

So cool.

Rodrigo Alves

Great idea!

Danilo Assis

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. Richard Schneeman

    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 Thompson Richard Schneeman

    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. Richard Schneeman
This page is out of date. Refresh to see the latest.
2  actionpack/CHANGELOG.md
View
@@ -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
19 actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
View
@@ -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
10 actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb
View
@@ -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>
121 actionpack/lib/action_dispatch/routing/inspector.rb
View
@@ -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
170 actionpack/test/dispatch/routing/inspector_test.rb
View
@@ -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
4 railties/lib/rails/application.rb
View
@@ -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
3  railties/lib/rails/info_controller.rb
View
@@ -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
4 railties/lib/rails/tasks/routes.rake
View
@@ -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.