MVC Handle 404s and set default error page - asp.net-mvc

I'm tying to enable the default routing in MVC.
I want every 404 request to redirect to DefaultController DefaultRout()
I found How can i make a catch all route to handle '404 page not found' queries for ASP.NET MVC?
But {*url} dosen't work i'm getting 404 and not redirecting to the default page.
My code:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("{resource}.aspx/{*pathInfo}");
routes.IgnoreRoute("{resource}.ascx/{*pathInfo}");
routes.IgnoreRoute("{resource}.ashx/{*pathInfo}");
routes.IgnoreRoute("{resource}.gif/{*pathInfo}");
//http://localhost:4775/BW/A/Tasks
routes.MapRoute("Pages", "A/{controller}", new { controller = "Tasks", action = "InitPage" });
routes.MapRoute(
"404-PageNotFound",
"{*url}",
new { controller = "Default", action = "DefaultRout" }
);
}
What am I missing?
Thanks
Rafael

Are you unable to use your web.config? I think this would be easier:
<customErrors mode="On" defaultRedirect="/error/default">
<error statusCode="403" redirect="/error/restricted"/>
<error statusCode="404" redirect="/Default/DefaultRoute"/>
<error statusCode="500" redirect="/error/problem"/>
</customErrors>

There are a few things that I would like to point here.
I notice that you have used many IgnoreRoute entries for physical files. You don't have to do that as the framework looks for the physical files matching the url by default before routing it. You can disable the physical file matching by turning RouteExistingFiles to true on the RouteCollection in Global.asax. In this case you haven't done that.
Secondly, the way you have set it up, any route but /A/{controller} will be caught by the catch all route (anything starting with * is a catch all route) that you have configured.
I have tried this configuration and it does catch all the other routes except the one mentioned above. One thing you have to keep in mind, however, is that the above configuration will still matching everything with the following type of url: /A/something/ because the second segment will always match the {controller} placeholder. To only match this url with the "Tasks" controller, you can define a constraint on the route as the following:
routes.MapRoute("Pages", "A/{controller}", new { controller = "Tasks", action = "InitPage" }, new {controller="Home"});
There is also a spelling mistake in your catch all route configuration. action = "DefaultRout" should be action = "DefaultRoute"
Hope this helps.

Related

ASP.NET MVC 4 - Exception Handling Not Working

When an error occurs in my ASP.NET MVC 4 application, I would like to customize a view for the user depending on the type of error. For example, page not found or an exception has occurred (with some user-friendly details about the exception). I've checked other examples of how to do this on StackOverflow and other online sources, but none of the answers are working for me.
The basic [HandleError] attribute does not seem to be working in VS2012 with an MVC 4 application targeting .NET 4.5. Here is the code I have in my home controller:
[HandleError]
public ActionResult Index()
{
Response.TrySkipIisCustomErrors = true; //doesn't work with or without this
throw new NullReferenceException("Uh oh, something broke.");
}
It is just throwing an exception, and I would expect the default ~/Shared/Error.cshtml view to be returned because of the [HandleError] attribute, but all I get is an HTTP 500 Internal Server Error indicating that the page could not be displayed. I checked my web.config, and different configurations seem to be behaving weird. In the section, it currently contains:
<customErrors mode="On" />
(I've tried adding defaultRedirect and with customErrors mode="Off" as well but that didn't have any effect... neither the shared Error view or the CustomError view I have is being rendered. If I change customErrors mode to off, then I can see the exception details as expected, so it is throwing the "Uh oh, something broke" exception properly.
I've also tried adding an OnException handler to the HomeController, and although I can debug through and see that the OnException event is being raised, it doesn't make any difference:
protected override void OnException(ExceptionContext filterContext)
{
base.OnException(filterContext);
filterContext.ExceptionHandled = true;
if (filterContext == null)
{
filterContext.Result = View("CustomError");
return;
}
Exception e = filterContext.Exception;
// TODO: Log the exception here
ViewData["Exception"] = e; // pass the exception to the view
filterContext.Result = View("CustomError");
}
I have also tried changing [HandleError] to specify a view, but that doesn't seem to do anything either:
[HandleError(View="CustomError")]
Any help would be much appreciated. Please let me know if you need any more details.
I also went on a seamingly endless journey of reading SO answers and assorted blog postings trying to get custom error pages to work. Below is what finally worked for me.
The first step is to use IIS Express for debugging instead of the built-in Cassini web server to "guarantee" that the debug experience will mirror the live environment.
Create a controller to handle application errors and add an action for each custom error you will handle. The action should do any logging, set the status code, and return the view.
public class ErrorsController : Controller
{
// 404
[HttpGet]
public ActionResult NotFound()
{
Response.StatusCode = (int)HttpStatusCode.NotFound;
return View();
}
// I also have test actions so that I can verify it's working in production.
[HttpGet]
public ActionResult Throw404()
{
throw new HttpException((int)HttpStatusCode.NotFound, "demo");
}
}
Configure the customErrors section in web.config to redirect to your custom error actions.
<system.web>
<customErrors mode="RemoteOnly" defaultRedirect="Errors/InternalServerError">
<error statusCode="400" redirect="Errors/BadRequest" />
<error statusCode="403" redirect="Errors/Forbidden" />
<error statusCode="404" redirect="Errors/NotFound" />
<error statusCode="500" redirect="Errors/InternalServerError" />
</customErrors>
Add the httpErrors section to system.webServer and set the errorMode to Detailed in web.config. Why this works is a mystery to me, but this was the key.
<system.webServer>
<httpErrors errorMode="Detailed" />
Add a catchall route last to the defined routes to direct 404s to the custom page.
// catchall for 404s
routes.MapRoute(
"Error",
"{*url}",
new {controller = "Errors", action = "NotFound"});
You may also want to create a custom HandleErrorAttribute and register it as a global filter to log 500 errors.
These steps worked for me in both development (IIS Express) and production (IIS7) environments. You need to change customErrors mode="On" to see the effect in development.
I seem to recall that you had to call the page from a non-localhost IP address (typically another computer). And it has to be an IIS based server, not the built-in development server (so IIS or IIS Express, but then you have to configure IIS Express for external access, which is a pain).
You can in fact debug it, you have to configure your local server on your debug computer to accept external requests, then call your local server from the remote server.
I faced a similar problem and lost sometime trying to work out what was going on. So just in case any others face a similar problem here is what my problem was.
The error page was trying to use my _Layout page. Just ensure that you Error.cshtml page has
#{
Layout = null;
}
Try adding the following attribute to the customErrors tag:
defaultRedirect="Error"
This explicitly defines to which view MVC should redirect the user when the error is thrown and no view is specified by default in the attribute. This will of course only work if Error.cshtml exists in the Shared views folder.

ASP.NET MVC 3: RouteExistingFiles = true seems to have no effect

I'm trying to understand how RouteExistingFiles works.
So I've created a new MVC 3 internet project (MVC 4 behaves the same way) and put a HTMLPage.html file to the Content folder of my project.
Now I went to the Global.Asax file and edited the RegisterRoutes function so it looks like this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.RouteExistingFiles = true; //Look for routes before looking if a static file exists
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new {controller = "Home", action = "Index", id = UrlParameter.Optional} // Parameter defaults
);
}
Now it should give me an error when I'm requesting a localhost:XXXX/Content/HTMLPage.html since there's no "Content" controller and the request definitely hits the default pattern. But instead I'm seeing my HTMLPage.
What am I doing wrong here?
Update:
I think I'll have to give up.
Even if I'm adding a route like this one:
routes.MapRoute("", "Content/{*anything}", new {controller = "Home", action = "Index"});
it still shows me the content of the HTMLPage.
When I request a url like ~/Content/HTMLPage I'm getting the Index page as expected, but when I add a file extenstion like .html or .txt the content is shown (or a 404 error if the file does not exist).
If anyone can check this in VS2012 please let me know what result you're getting.
Thank you.
To enabling routing for static files you must perform following steps.
In RouteConfig.cs enable routing for existing files
routes.RouteExistingFiles = true;
Add a route for your path ( Make sure specialized path are above generalized paths)
routes.MapRoute(
name: "staticFileRoute",
url: "Public/{file}/",
defaults: new { controller = "Home", action = "SomeAction" }
);
Next configure your application, so that request for static files are handeled by "TransferRequestHandler".In Webconfig under system.webServer>handlers add following entry.
<add name="MyCustomUrlHandler2" path="Public/*" verb="GET" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
The value of 'path' can be more generic or specific depending on your requirement. But i prefer it to be always very specific as per one's need. Keeping it very generic will block serving of other site specific resources such as .js or css files. For example if above is set as path="*", then request for even the css (inside the content folder) which is responsible for how your page would look will also end up in your Controller's action. Something that you will not like.
Visual Studio 2012 uses IIS Express. You need to tell IIS not to intercept requests for disk files before they are passed to the MVC routing system. You need set preCondition attribute to the empty string in config file:
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule"
preCondition="" />
In Win7/8 you can find config file on this path: %userprofile%\Documents\IISExpress\config\applicationhost.config
The RouteExistingFiles doesn't keep files from being viewed if there is no route for them, it just checks the routes before checking if the file exists. If there is no matching route, it will continue to check if there is a matching file.

Correct 404 message for a missing ASP.Net MVC controller

I have an MVC 2 application that should always give a 'nice' 404 page.
However currently I get a low level .Net one: "Server Error in '/sitename' Application..."
I have a base controller that has a NotFound action that will render the nice 404 page.
Missing actions are handled:
protected override void HandleUnknownAction(string actionName)
{
this.NotFound(actionName).ExecuteResult(this.ControllerContext);
}
So a visit to {site}/ValidController/NotAnAction gets routed correctly.
However a visit to {site}/NotAController doesn't.
I have routes set up with a catch all:
routes.MapRoute(
"MVC routes",
"{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional });
routes.MapRoute(
"Catch All",
"{*url}",
new { controller = "System", action = "NotFound" });
The catch all correctly catches routes that don't match.
So {site}/Invalid/Action/id/extra is correctly routed via the catch all.
However {site}/Invalid gets picked up via the "MVC routes" route and ASP.Net goes looking for InvalidController, and throws a dumb exception when it doesn't find it.
I know that I can override this at the web.config level, but that just redirects to the page. I'd like to know when the route pattern matches but the controller is not a valid controller name.
Where can I catch and change this behaviour?
I finally found the answer to this, though it's still not ideal.
You can restrict the controller names that are allowed to match a route using a regex, so if we assume the default implementation of the controller factory we can figure out all the possible class names that are supported:
// build up a list of known controllers, so that we don't let users hit ones that don't exist
var allMvcControllers =
from t in typeof(Global).Assembly.GetTypes()
where t != null &&
t.IsPublic &&
!t.IsAbstract &&
t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
typeof(IController).IsAssignableFrom(t)
select t.Name.Substring(0, t.Name.Length - 10);
// create a route constraint that requires the controller to be one of the reflected class names
var controllerConstraint = new
{
controller = "(" + string.Join("|", allMvcControllers.ToArray()) + ")"
};
// default MVC route
routes.MapRoute(
"MVC",
"{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
controllerConstraint);
// fall back route for unmatched patterns or invalid controller names
routes.MapRoute(
"Catch All",
"{*url}",
new { controller = "System", action = "NotFound" });
This isn't ideal, it adds a hit on the application start and still feels far too complicated, but it does have the desired effect.
Setting it up at web.config level does not "just redirect to the page". On an MVC app, you give it a "{controller/action}" url an it will actually call that action:
<customErrors mode="On" defaultRedirect="/system/problem">
<error statusCode="404" redirect="/system/notfound" />
</customErrors>
This will call the NotFound on the SystemController.
In your action you can then for instance get the value of HttpContext.Request.RawUrl to see what the faulty request was: "/system/notfound?aspxerrorpath=/Invalid". In this case I tried to go to the InvalidController.
A nice way to handle this things, by the way, is implementing ELMAH (or Error Logging Modules and Handlers. Scott Hanselman wrote an "introductory" post about it, but ELMAH is nowadays available as a NuGet package.
You might want to take a look at this question/ansers on how to use it with ASP.NET MVC: How to get ELMAH to work with ASP.NET MVC [HandleError] attribute?

ASP.NET MVC - Catch All Route And Default Route

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?
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" }
);
//this catches all requests
routes.MapRoute(
"Error",
"{*.}",
new { controller = "PublicDisplay", action = "Error404" }
);
add this route at the end the routes table
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.
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.
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.
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
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" }
);

Can't find the custom error page

Here's how I have it defined (locally, on my development machine):
<customErrors mode="On" defaultRedirect="Error.aspx">
<error statusCode="404" redirect="NotFound.aspx" />
</customErrors>
And I have the [HandleError] attribute:
[Authorize]
[HandleError]
public class HomeController : Controller
{ // etc.
Yet when I type in http://localhost:1986/blah, I get the following error:
The resource cannot be found.
Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the following URL and make sure that it is spelled correctly.
Requested URL: /NotFound.aspx
The URL it's trying to go to is as you would expect:
http://localhost:1986/NotFound.aspx?aspxerrorpath=/blah
So it IS attempting to go to the custom error file -- however it can't find it. I do have NotFound.aspx in the Shared directory -- same place as the Error.aspx supplied by Microsoft as a default. Why can't it find it?
If the Error.aspx and NotFound.aspx are in the shared directory is there a controller wired to served them? If you do not have some sort of controller route configured to serve the files then the fact that they are in the shared folder is irrelevant.
You have a few options, you could create an ErrorController which will handle the requests for those views and define routes pointing to those controller actions:
[OutputCache(CacheProfile = "Default", VaryByParam = "none")]
public class ErrorController : DefaultAreaBaseController
{
public ViewResult ServiceUnavailable() {
Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
return View("ServiceUnavailable");
}
public ViewResult ServerError() {
Response.StatusCode = (int)HttpStatusCode.InternalServerError;
return View("ServerError");
}
public new ViewResult NotFound() {
Response.StatusCode = (int)HttpStatusCode.NotFound;
return View("NotFound");
}
}
Or as an alternative, you can create ignore routes pointing at the physical files and place the error pages somewhere other than the Views folder (like your root directory):
routes.IgnoreRoute("Error.aspx/{*pathInfo}");
routes.IgnoreRoute("NotFound.aspx/{*pathInfo}");
Either of these solutions is viable however depending on your configuration using an IgnoreRoute() may be more ideal as it will forgo the need to pipe the request to MVC only to serve a static error page.
Option One:
is to build an Errors Controller with a "NotFound" view along with a "Unknown" view. This will take anything that is a 500 Server error or a 404 NotFound error and redirect you to the appropriate URL. I don't totally love this solution as the visitor is always redirected to an error page.
http://example.com/Error/Unknown
<customErrors mode="On" defaultRedirect="Error/Unknown">
<error statusCode="404" redirect="Error/NotFound" />
<error statusCode="500" redirect="Error/Unknown" />
</customErrors>
wwwroot/
Controllers
Error.cs
Views/
Error/
NotFound.aspx
Unknown.aspx
Option Two:
I Definitely don't prefer this method (as it is basically reverting back to web forms, The second option is to simply have a static Error.aspx page and ignore the route in MVC), but it works none the less. What you're doing here is ignoring a "Static" directory, placing your physical Error pages in there, and skirting around MVC.
routes.IgnoreRoute("/Static/{*pathInfo}"); //This will ignore everything in the "Static" directory
wwwroot/
Controllers/
Static/
Error.aspx
Views/
Option Three:
The third option (THIS IS MY FAVORITE) is to return an Error View from whatever view is catching the error. This would require you to code up Try/Catch blocks along the way for "known" errors and then you can use HandleError for the unknown errors that might creep up. What this will do is preserve the originally requested URL but return the ERROR view.
EXAMPLE:
http://example.com/Products/1234 will show a details page for ProductID 1234
http://example.com/Products/9999 will show a NotFound error page because ProductID 9999 doesn't exist
http://example.com/Errors/NotFound "should" never be shown because you handle those errors individually in your controllers.
Web.Config
<customErrors mode="On">
</customErrors>
Controller
// Use as many or as few of these as you need
[HandleError(ExceptionType = typeof(SqlException), View = "SqlError")]
[HandleError(ExceptionType = typeof(NullReferenceException), View = "NullError")]
[HandleError(ExceptionType = typeof(SecurityException), View = "SecurityError")]
[HandleError(ExceptionType = typeof(ResourceNotFoundException), View = "NotFound")]
Public Class ProductController: Controller{
public ViewResult Item(string itemID)
{
try
{
Item item = ItemRepository.GetItem(itemID);
return View(item);
}
catch()
{
return View("NotFound");
}
}
}
Folder Structure
wwwroot/
Controllers/
Shared/
NotFound.aspx
NullError.aspx
SecurityError.aspx
SqlError.aspx
Views/
Option Four:
The last option would be that you build your own custom filter for things like ResourceNotFoundException and attach it to your controller class. This will do the exact same thing as above but with the added benefit of sending the error code down the line to the client as well.
Richard Dingwall talks about it on his blog.
Your mixing web forms and MVC concepts here. Turn custom errors off in the web.config. Then in the HandleError attribute optionally specify the type and view, by default error.aspx is searched for in views\CurrentController then views\shared. Whilst you can get 404 handling working with the HandleError filter you probably want to create a filter just for 404 handling, the reasons and how to are explained in detail here:
http://richarddingwall.name/2008/08/17/strategies-for-resource-based-404-errors-in-aspnet-mvc/

Resources