76

In trying to get my application to produce 404 errors correctly, I have implemented a catch all route at the end of my route table, as shown below:

 routes.MapRoute(
            "NotFound", _
           "{*url}", _
           New With {.controller = "Error", .action = "PageNotFound"} _
       )

However, to get this working, I had to remove the default route:

{controller}/action/{id}

But now that the default has been removed, most of my action links no longer work, and the only way I have found to get them working again is to add individual routes for each controller/action.

Is there a simpler way of doing this, rather than adding a route for each controller/action?

Is it possible to create a default route that still allows the catch all route to work if the user tries to navigate to an unknown route?

Sean Taylor
  • 4,948
  • 8
  • 25
  • 27

7 Answers7

114

Use route constraints

In your case you should define your default route {controller}/{action}/{id} and put a constraint on it. Probably related to controller names or maybe even actions. Then put the catch all one after it and it should work just fine.

So when someone would request a resource that fails a constraint the catch-all route would match the request.

So. Define your default route with route constraints first and then the catch all route after it:

routes.MapRoute(
    "Default",
    "{controller}/{action}/{id}",
    new { controller = "Home", action = "Index", id = UrlParameter.Optional },
    new { controller = "Home|Settings|General|..." } // this is basically a regular expression
);
routes.MapRoute(
    "NotFound",
    "{*url}",
    new { controller = "Error", action = "PageNotFound" }
);
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
Robert Koritnik
  • 103,639
  • 52
  • 277
  • 404
  • Good point. I thought the first route would match only if the controller name matches... Thank you! – Andrei Rînea Feb 26 '15 at 19:46
  • @andreirinea no, routing map isn't checking any types. It only checks whether current request's URL matches mapping. The only thing each route checks are constraints when present. – Robert Koritnik Feb 26 '15 at 21:09
  • 16
    You could use reflection to build your controller constraint and thus avoid a maintenance burden every time you add a new controller. `private static string GetAllControllersAsRegex() { var controllers = typeof(MvcApplication).Assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(Controller))); var controllerNames = controllers.Select(c => c.Name.Replace("Controller", "")); return string.Format("({0})", string.Join("|", controllerNames)); }` – Blisco Feb 27 '15 at 15:58
  • 1
    Is this the correct way to implement 404 error on url that are not resolved by route. Or is it just a workaround and cleaner solution exist. I find it funny that in 2015 you have to manualy handle something that should be done by server. – f470071 Jun 03 '15 at 09:20
  • 1
    @f470071: You're right. In normal conditions this is easily handled by web server but sometimes applications have specific constraints that should allow arbitrary resource querying. The only **strange** thing that OP wants to do is to handle errors in their app while they could do so using usual web server capabilities. I would also leave out the catch-all route definition at the end and just use constrained routing as outlined in my answer. – Robert Koritnik Jun 03 '15 at 13:51
  • @Robert I know this is not the place but could you take a look at my question http://stackoverflow.com/questions/30619301/asp-net-mvc-how-to-return-correct-response-code?noredirect=1#comment49310338_30619301. I have a problem that I consistently get 500 error response instead of 404. – f470071 Jun 03 '15 at 13:56
  • @RobertKoritnik , MVC 5, possible to deny Direct action access only allow route, example action is= AuctionOne/Index route=one/index is it possible to deny action but not route..?! – SAR Jan 01 '17 at 05:28
  • @SAR I'm sorry, but I don't understand what you're asking? Denying actions is done using `AuthorizeAttribute` or at least some `ActionMethodSelectorAttribute` derived class. If that's what you're looking for... – Robert Koritnik Jan 04 '17 at 01:22
  • @RobertKoritnik I mean in address sending class name and it action should be restricted but allow it only if it is in rout thank you – SAR Jan 04 '17 at 04:17
  • @SAR. I'm trying my best to figure out your question... So when you say *direct action access* what do you mean by that? If routing parses URL to your `Controller.Action(...)` method, then this action will get executed. In your case (as I understand) URL is `.../one/index` which points by routing configuration to `ActionOneController.Index(...)` action method. Which action method would you want to execute when you *allow route, but deny action method*? Something has to be executed otherwise there will be an exception. – Robert Koritnik Jan 05 '17 at 15:33
24
//this catches all  requests
routes.MapRoute(
    "Error",
    "{*.}",
     new { controller = "PublicDisplay", action = "Error404" } 
);

add this route at the end the routes table

Praveen Prasad
  • 31,561
  • 18
  • 73
  • 106
  • 6
    Surely this catches all request. But if you declare it before the default route, it will override the default route and if you declare it after the default route, it won't be called because the default route catches the request. – Tobias Jan 28 '16 at 09:06
  • 2
    `"{*.*}",` worked for me on asp.net vb as a catchall – bendecko Jun 22 '16 at 18:27
  • `"{*.}"` was only catching routes without an extension for me. Changed to `"{*.*}"` to catch all routes, including .aspx. – Keith Sirmons Mar 21 '17 at 19:25
7

Ah, the problem is your default route catches all 3 segment URLs. The issue here is that Routing runs way before we determine who is going to handle the request. Thus any three segment URL will match the default route, even if it ends up later that there's no controller to handle it.

One thing you can do is on your controller override the HandleMissingAction method. You should also use the tag to catch all 404 issues.

Haacked
  • 58,045
  • 14
  • 90
  • 114
2

Well, what I have found is that there is no good way to do this. I have set the redirectMode property of the customErrors to ResponseRewrite.

<customErrors mode="On" defaultRedirect="~/Shared/Error" redirectMode="ResponseRewrite">
    <error statusCode="404" redirect="~/Shared/PageNotFound"/>
</customErrors>

This gives me the sought after behavior, but does not display the formatted page.

To me this is poorly done, as far as SEO goes. However, I feel there is a solution that I am missing as SO does exactly what I want to happen. The URL remains on the failed page and throws a 404. Inspect stackoverflow.com/fail in Firebug.

Dustin Laine
  • 37,935
  • 10
  • 86
  • 125
  • Funny you should say that, when I first started looking at the issue, I first checked to see how SO handles it. I could manually add routes for each action, but I think this would be hard to maintain over time. I really think that MS could provide better guidance on the issue. – Sean Taylor Oct 22 '10 at 22:11
  • 3
    Maybe if we ask Jeff nicely he will share it with us? :) – Sean Taylor Oct 22 '10 at 22:14
  • After much digging around, trying out various solutions, this one seems to work ok for me. http://stackoverflow.com/questions/619895/how-can-i-properly-handle-404s-in-asp-net-mvc/2577095#2577095 – Sean Taylor Oct 23 '10 at 10:47
1

I would recommend this as the most readable version. You need this in your RouteConfig.cs, and a controller called ErrorController.cs, containing an action 'PageNotFound'. This can return a view. Create a PageNotFound.cshtml, and it'll be returned in response to the 404:

        routes.MapRoute(
            name:  "PageNotFound",
            url:  "{*url}",
            defaults: new { controller = "Error", action = "PageNotFound" }
        );

How to read this:

name: "PageNotFound"

= create a new route template, with the arbitrary name 'PageNotFound'

url:"{*url}"

= use this template to map all otherwise unhandled routes

defaults: new { controller = "Error", action = "PageNotFound" }

= define the action that an incorrect path will map to (the 'PageNotFound' Action Method in the Error controller). This is needed since an incorrectly entered path will not obviously not map to any action method

Chris Halcrow
  • 28,994
  • 18
  • 176
  • 206
1

My Solution is 2 steps.

I originally solved this problem by adding this function to my Global.asax.cs file:

protected void Application_Error(Object sender, EventArgs e)

Where I tried casting Server.GetLastError() to a HttpException, and then checked GetHttpCode. This solution is detailed here:

ASP.NET MVC Custom Error Handling Application_Error Global.asax?

This isn't the original source where I got the code. However, this only catches 404 errors which have already been routed. In my case, that ment any 2 level URL.

for instance, these URLs would display the 404 page:

www.site.com/blah

www.site.com/blah/blah

however, www.site.com/blah/blah/blah would simply say page could not be found. Adding your catch all route AFTER all of my other routes solved this:

routes.MapRoute(
            "NotFound",
            "{*url}",
            new { controller = "Errors", action = "Http404" }
        );

However, the NotFound route does not seem to route requests which have file extensions. This does work when they are captured by different routes.

Community
  • 1
  • 1
Marcus10110
  • 509
  • 4
  • 12
0

I tried all of the above pattern without luck, but finally found out that ASP.NET considered the url I used as a static file, so none of my request was hidding the single controller endpoint. I ended up adding this snipper to the web.config

<modules runAllManagedModulesForAllRequests="true"/>

And then use the below route match pattern, and it solved the issue:

 routes.MapRoute(
            name:  "RouteForAnyRequest",
            url:  "{*url}",
            defaults: new { controller = "RouteForAnyRequest", action = "PageNotFound" }
        );
Kjeld Poulsen
  • 213
  • 3
  • 4