Skip to content
This repository

Add Missing Keys from Journey on Failed URL Format #7230

Merged
merged 2 commits into from over 1 year ago

6 participants

Richard Schneeman Rafael Mendonça França Steve Klabnik Andrew White Carlos Antonio da Silva Jon Leighton
Richard Schneeman
Collaborator

Many named routes have keys that are required to successfully resolve. If a key is left off like this:

<%= link_to 'user', user_path %>

An error will be raised like this:

No route matches {:action=>"show", :controller=>"users"}

Since Journey know's that the :id is missing, we can add extra debugging information to the error message.

No route matches {:action=>"show", :controller=>"users"} missing required keys: [:id]

This will help new and seasoned developers look closer at their parameters. I've also subclassed the routing error to be clear that this error is a result of attempting to generate a url and not because the user is trying to visit a bad url.

The current error message is misleading and confuses most developers. The important part isn't what's in the options, the important part is that we are missing keys. Adding this information to the error message will make debugging much more obvious.

This is the sister pull request of rails/journey#44 which will be required to get they missing keys into the correct error message. Opening both issues at the same time to start a dialog.

Example Development Error in Rails: http://cl.ly/image/3S0T0n1T3421

cc/ @pixeltrix

Rafael Mendonça França
Owner

Cool. :+1:

Steve Klabnik
Collaborator

I like it! :+1:

Andrew White
Owner

@schneems can you add a test to make sure that raising an ActionController::UrlGenerationError returns a 500 response code and not a 404 - thanks!

Richard Schneeman
Collaborator

Added a test, let me know if there is a better place, or if you want more: https://github.com/rails/rails/pull/7230/files#L3R118

Carlos Antonio da Silva

Looks nice! Thanks @schneems.

Rafael Mendonça França
Owner

Could you rebase this one, add a CHANGELOG and update all the related docs? I'm going to merge it since rails/journey#44 was accepted.

actionpack/lib/action_dispatch/routing/route_set.rb
((8 lines not shown))
556 556
         end
557 557
 
558  
-        def raise_routing_error
559  
-          raise ActionController::RoutingError, "No route matches #{options.inspect}"
  558
+        def raise_routing_error(message = "")
  559
+          raise ActionController::UrlGenerationError, "No route matches #{options.inspect} #{message}"
2
Rafael Mendonça França Owner

This is something that bother me, but if we don't have message we still have a trailing space after the options.

Andrew White Owner
pixeltrix added a note August 27, 2012

Is raise_routing_error ever called without a message? Does the message argument need to be optional?

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

@pixeltrix @rafaelfranca I also refactored some of the code so we can get rid of the error method. This gets rid of the whitespace problem and cleans up the interface to the generator class :) If you're not fans, I can revert. schneems@5e55f91 ATP actionpack & railties.

I checked the guides and greped around, didn't see any docs containing this error message. Added a changelog entry.

Rafael Mendonça França
Owner

Seems good. Now it needs a rebase.

added some commits August 01, 2012
Richard Schneeman Add Missing Keys from Journey on failed URL format
Many named routes have keys that are required to successfully resolve. If a key is left off like this:

    <%= link_to 'user', user_path %>

This will produce an error like this:

    No route matches {:action=>"show", :controller=>"users"}

Since we know that the :id is missing, we can add extra debugging information to the error message.

    No route matches {:action=>"show", :controller=>"users"} missing required keys: [:id]


This will help new and seasoned developers look closer at their parameters. I've also subclassed the routing error to be clear that this error is a result of attempting to generate a url and not because the user is trying to visit a bad url. 

While this may sound trivial this error message is misleading and confuses most developers. The important part isn't what's in the options its's what's missing. Adding this information to the error message will make debugging much more obvious. 

This is the sister pull request of rails/journey#44 which will be required to get they missing keys into the correct error message. 

Example Development Error in Rails: http://cl.ly/image/3S0T0n1T3421
0b6175a
Richard Schneeman refactor route_set `generate_extras` functionality
The result of Generator with or without the @extras instance variable set contains the desired information. Rather than preserving state when initializing the original object, we can simply extract the keys from the resultant parameters.

ATP Actionpack, railties
cc57b2a
Richard Schneeman
Collaborator

Code has been rebased, ready for merge. Let me know if I need to do anything else.

Andrew White pixeltrix merged commit 46d8543 into from August 29, 2012
Andrew White pixeltrix closed this August 29, 2012
Andrew White
Owner

Thanks for your work on this @schneems - hopefully it should reduce the number of false reports we get about routes not matching.

Jon Leighton
Owner

great job :+1:

Richard Schneeman
Collaborator

Thanks for all the eyes and all the help, you all rock!

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

Showing 2 unique commits by 1 author.

Aug 28, 2012
Richard Schneeman Add Missing Keys from Journey on failed URL format
Many named routes have keys that are required to successfully resolve. If a key is left off like this:

    <%= link_to 'user', user_path %>

This will produce an error like this:

    No route matches {:action=>"show", :controller=>"users"}

Since we know that the :id is missing, we can add extra debugging information to the error message.

    No route matches {:action=>"show", :controller=>"users"} missing required keys: [:id]


This will help new and seasoned developers look closer at their parameters. I've also subclassed the routing error to be clear that this error is a result of attempting to generate a url and not because the user is trying to visit a bad url. 

While this may sound trivial this error message is misleading and confuses most developers. The important part isn't what's in the options its's what's missing. Adding this information to the error message will make debugging much more obvious. 

This is the sister pull request of rails/journey#44 which will be required to get they missing keys into the correct error message. 

Example Development Error in Rails: http://cl.ly/image/3S0T0n1T3421
0b6175a
Richard Schneeman refactor route_set `generate_extras` functionality
The result of Generator with or without the @extras instance variable set contains the desired information. Rather than preserving state when initializing the original object, we can simply extract the keys from the resultant parameters.

ATP Actionpack, railties
cc57b2a
This page is out of date. Refresh to see the latest.
5  actionpack/CHANGELOG.md
Source Rendered
... ...
@@ -1,5 +1,10 @@
1 1
 ## Rails 4.0.0 (unreleased) ##
2 2
 
  3
+*   When building a URL fails, add missing keys provided by Journey. Failed URL
  4
+    generation now returns a 500 status instead of a 404.
  5
+
  6
+    *Richard Schneeman*
  7
+
3 8
 *   Deprecate availbility of ActionView::RecordIdentifier in controllers by default.
4 9
     It's view specific and can be easily included in controller manually if someone
5 10
     really needs it. RecordIdentifier will be removed from ActionController::Base
3  actionpack/lib/action_controller/metal/exceptions.rb
@@ -16,6 +16,9 @@ def initialize(message, failures=[])
16 16
     end
17 17
   end
18 18
 
  19
+  class ActionController::UrlGenerationError < RoutingError #:nodoc:
  20
+  end
  21
+
19 22
   class MethodNotAllowed < ActionControllerError #:nodoc:
20 23
     def initialize(*allowed_methods)
21 24
       super("Only #{allowed_methods.to_sentence(:locale => :en)} requests are allowed.")
28  actionpack/lib/action_dispatch/routing/route_set.rb
@@ -438,12 +438,11 @@ class Generator #:nodoc:
438 438
 
439 439
         attr_reader :options, :recall, :set, :named_route
440 440
 
441  
-        def initialize(options, recall, set, extras = false)
  441
+        def initialize(options, recall, set)
442 442
           @named_route = options.delete(:use_route)
443 443
           @options     = options.dup
444 444
           @recall      = recall.dup
445 445
           @set         = set
446  
-          @extras      = extras
447 446
 
448 447
           normalize_options!
449 448
           normalize_controller_action_id!
@@ -526,20 +525,12 @@ def handle_nil_action!
526 525
           recall[:action] = options.delete(:action) if options[:action] == 'index'
527 526
         end
528 527
 
  528
+        # Generates a path from routes, returns [path, params]
  529
+        # if no path is returned the formatter will raise Journey::Router::RoutingError
529 530
         def generate
530  
-          path, params = @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE)
531  
-
532  
-          raise_routing_error unless path
533  
-
534  
-          return [path, params.keys] if @extras
535  
-
536  
-          [path, params]
537  
-        rescue Journey::Router::RoutingError
538  
-          raise_routing_error
539  
-        end
540  
-
541  
-        def raise_routing_error
542  
-          raise ActionController::RoutingError, "No route matches #{options.inspect}"
  531
+          @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE)
  532
+        rescue Journey::Router::RoutingError => e
  533
+          raise ActionController::UrlGenerationError, "No route matches #{options.inspect} #{e.message}"
543 534
         end
544 535
 
545 536
         def different_controller?
@@ -564,11 +555,12 @@ def extra_keys(options, recall={})
564 555
       end
565 556
 
566 557
       def generate_extras(options, recall={})
567  
-        generate(options, recall, true)
  558
+        path, params = generate(options, recall)
  559
+        return path, params.keys
568 560
       end
569 561
 
570  
-      def generate(options, recall = {}, extras = false)
571  
-        Generator.new(options, recall, self, extras).generate
  562
+      def generate(options, recall = {})
  563
+        Generator.new(options, recall, self).generate
572 564
       end
573 565
 
574 566
       RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
12  actionpack/test/controller/routing_test.rb
@@ -275,7 +275,7 @@ def test_specific_controller_action_failure
275 275
       mount lambda {} => "/foo"
276 276
     end
277 277
 
278  
-    assert_raises(ActionController::RoutingError) do
  278
+    assert_raises(ActionController::UrlGenerationError) do
279 279
       url_for(rs, :controller => "omg", :action => "lol")
280 280
     end
281 281
   end
@@ -514,7 +514,7 @@ def test_should_list_options_diff_when_routing_constraints_dont_match
514 514
     rs.draw do
515 515
       get 'post/:id' => 'post#show', :constraints => { :id => /\d+/ }, :as => 'post'
516 516
     end
517  
-    assert_raise(ActionController::RoutingError) do
  517
+    assert_raise(ActionController::UrlGenerationError) do
518 518
       url_for(rs, { :controller => 'post', :action => 'show', :bad_param => "foo", :use_route => "post" })
519 519
     end
520 520
   end
@@ -594,7 +594,7 @@ def test_requirement_should_prevent_optional_id
594 594
 
595 595
     assert_equal '/post/10', url_for(rs, { :controller => 'post', :action => 'show', :id => 10 })
596 596
 
597  
-    assert_raise ActionController::RoutingError do
  597
+    assert_raise(ActionController::UrlGenerationError) do
598 598
       url_for(rs, { :controller => 'post', :action => 'show' })
599 599
     end
600 600
   end
@@ -760,7 +760,7 @@ def test_failed_constraints_raises_exception_with_violated_constraints
760 760
       get 'foos/:id' => 'foos#show', :as => 'foo_with_requirement', :constraints => { :id => /\d+/ }
761 761
     end
762 762
 
763  
-    assert_raise(ActionController::RoutingError) do
  763
+    assert_raise(ActionController::UrlGenerationError) do
764 764
       setup_for_named_route.send(:foo_with_requirement_url, "I am Against the constraints")
765 765
     end
766 766
   end
@@ -1051,7 +1051,7 @@ def test_route_error_with_missing_controller
1051 1051
     set.draw do
1052 1052
       get    "/people" => "missing#index"
1053 1053
     end
1054  
-    
  1054
+
1055 1055
     assert_raise(ActionController::RoutingError) {
1056 1056
       set.recognize_path("/people", :method => :get)
1057 1057
     }
@@ -1459,7 +1459,7 @@ def test_route_requirement_generate_with_ignore_case
1459 1459
 
1460 1460
     url = url_for(set, { :controller => 'pages', :action => 'show', :name => 'david' })
1461 1461
     assert_equal "/page/david", url
1462  
-    assert_raise ActionController::RoutingError do
  1462
+    assert_raise(ActionController::UrlGenerationError) do
1463 1463
       url_for(set, { :controller => 'pages', :action => 'show', :name => 'davidjamis' })
1464 1464
     end
1465 1465
     url = url_for(set, { :controller => 'pages', :action => 'show', :name => 'JAMIS' })
11  actionpack/test/dispatch/debug_exceptions_test.rb
@@ -37,6 +37,8 @@ def call(env)
37 37
         raise ActionView::Template::Error.new('template', AbstractController::ActionNotFound.new)
38 38
       when "/bad_request"
39 39
         raise ActionController::BadRequest
  40
+      when "/missing_keys"
  41
+        raise ActionController::UrlGenerationError, "No route matches"
40 42
       else
41 43
         raise "puke!"
42 44
       end
@@ -113,6 +115,15 @@ def call(env)
113 115
     assert_match(/AbstractController::ActionNotFound/, body)
114 116
   end
115 117
 
  118
+  test "named urls missing keys raise 500 level error" do
  119
+    @app = DevelopmentApp
  120
+
  121
+    get "/missing_keys", {}, {'action_dispatch.show_exceptions' => true}
  122
+    assert_response 500
  123
+
  124
+    assert_match(/ActionController::UrlGenerationError/, body)
  125
+  end
  126
+
116 127
   test "show the controller name in the diagnostics template when controller name is present" do
117 128
     @app = DevelopmentApp
118 129
     get("/runtime_error", {}, {
12  actionpack/test/dispatch/routing_test.rb
@@ -1799,7 +1799,7 @@ def test_constraints_are_merged_from_scope
1799 1799
 
1800 1800
     get '/movies/00001'
1801 1801
     assert_equal 'Not Found', @response.body
1802  
-    assert_raises(ActionController::RoutingError){ movie_path(:id => '00001') }
  1802
+    assert_raises(ActionController::UrlGenerationError){ movie_path(:id => '00001') }
1803 1803
 
1804 1804
     get '/movies/0001/reviews'
1805 1805
     assert_equal 'reviews#index', @response.body
@@ -1807,7 +1807,7 @@ def test_constraints_are_merged_from_scope
1807 1807
 
1808 1808
     get '/movies/00001/reviews'
1809 1809
     assert_equal 'Not Found', @response.body
1810  
-    assert_raises(ActionController::RoutingError){ movie_reviews_path(:movie_id => '00001') }
  1810
+    assert_raises(ActionController::UrlGenerationError){ movie_reviews_path(:movie_id => '00001') }
1811 1811
 
1812 1812
     get '/movies/0001/reviews/0001'
1813 1813
     assert_equal 'reviews#show', @response.body
@@ -1815,7 +1815,7 @@ def test_constraints_are_merged_from_scope
1815 1815
 
1816 1816
     get '/movies/00001/reviews/0001'
1817 1817
     assert_equal 'Not Found', @response.body
1818  
-    assert_raises(ActionController::RoutingError){ movie_path(:movie_id => '00001', :id => '00001') }
  1818
+    assert_raises(ActionController::UrlGenerationError){ movie_path(:movie_id => '00001', :id => '00001') }
1819 1819
 
1820 1820
     get '/movies/0001/trailer'
1821 1821
     assert_equal 'trailers#show', @response.body
@@ -1823,7 +1823,7 @@ def test_constraints_are_merged_from_scope
1823 1823
 
1824 1824
     get '/movies/00001/trailer'
1825 1825
     assert_equal 'Not Found', @response.body
1826  
-    assert_raises(ActionController::RoutingError){ movie_trailer_path(:movie_id => '00001') }
  1826
+    assert_raises(ActionController::UrlGenerationError){ movie_trailer_path(:movie_id => '00001') }
1827 1827
   end
1828 1828
 
1829 1829
   def test_only_should_be_read_from_scope
@@ -2142,7 +2142,7 @@ def test_nested_resource_constraints
2142 2142
 
2143 2143
     get '/lists/2/todos/1'
2144 2144
     assert_equal 'Not Found', @response.body
2145  
-    assert_raises(ActionController::RoutingError){ list_todo_path(:list_id => '2', :id => '1') }
  2145
+    assert_raises(ActionController::UrlGenerationError){ list_todo_path(:list_id => '2', :id => '1') }
2146 2146
   end
2147 2147
 
2148 2148
   def test_named_routes_collision_is_avoided_unless_explicitly_given_as
@@ -2625,7 +2625,7 @@ def app; Routes end
2625 2625
 
2626 2626
     get "/categories/1"
2627 2627
     assert_response :success
2628  
-    assert_raises(ActionController::RoutingError) { product_path(nil) }
  2628
+    assert_raises(ActionController::UrlGenerationError) { product_path(nil) }
2629 2629
   end
2630 2630
 end
2631 2631
 
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.