When developing a project on ASP.NET MVC, it became necessary to make my own 404 error page. I expected that I would cope with this task in a few minutes. After 6 hours of work, I identified two options for its solution of varying degrees of complexity. Description - further.
In ASP.NET MVC 3, with which I work, global filters have appeared. In the template of the new project, its use is already built in to display its own error page (via
HandleErrorAttribute ). Remarkable method, but he can not handle errors with code 404 (page not found). So I had to look for another beautiful way to handle this error.
404 error handling via Web.config
ASP.NET platform provides the ability to arbitrarily handle errors by uncomplicated configuration of the Web.config file. For this, the following code should be added to the system.web section:
<customErrors mode="On" > <error statusCode="404" redirect="/Errors/Error404/" /> </customErrors>
When an error 404 occurs, the user will be sent to yoursite.ru/Errors/Error404/?aspxerrorpath=/Not/Found/Url page. Besides the fact that it is not very nice and convenient (you cannot edit the url), it’s also bad for SEO -
an article on habr.ru.The method can be slightly improved by adding redirectMode = "ResponseRewrite" to customErrors:
<customErrors mode="On" redirectMode="ResponseRewrite" > <error statusCode="404" redirect="/Errors/Error404/" /> </customErrors>
In this case, it should not be a redirect to the error handling page, but a substitution of the requested error path by the contents of the specified page. However, there are some difficulties. On ASP.NET MVC, this method does not work as shown. A sufficiently detailed discussion (in English) can be found in the
topic . In short, this method is based on the Server.Transfer method, which is used in classic ASP.NET and, accordingly, works only with static files. It refuses to work with dynamic pages, as in the example (since it does not see the '/ Errors / Error404 /' file on the disk). That is, if you replace '/ Errors / Error404 /' with, for example, '/Errors/Error404.htm', then the described method will work. However, in this case, it will not be possible to perform additional actions on error handling, for example, logging.
In this topic, it was suggested to add the following code to each page:
Response.TrySkipIisCustomErrors = true;
This method only works with IIS 7 and higher, so it was not possible to test this method - we use IIS 6. The search had to be continued.
')
Dances with a tambourine and Application_Error
If the method described above cannot be applied for any reason, then you will have to write more lines of code. A partial solution is given in the
article .
I found the most complete solution with a tambourine in the
topic . The discussion is conducted in English, so I will translate the text of the decision into Russian.
Below are my requirements for solving the problem of displaying a 404 NotFound error:
- I want to handle paths for which no action is defined.
- I want to handle paths for which no controller is defined.
- I want to handle paths that my application failed to parse. I do not want these errors to be processed in Global.asax or IIS, because then I will not be able to redirect back to my application.
- I want to process my own (for example, when the required product is not found by ID) error 404 in the same style.
- I want all 404 errors to return MVC View, rather than a static page, so that I can get more error data. And they should return a 404 status code.
I think that Application_Error in Global.asax should be used for higher-level purposes, for example, to handle unhandled exceptions or logging, and not to work with error 404. Therefore, I try to put all the code associated with error 404 outside the Global file. asax.
Step 1: Create a common place to handle 404 error
This will facilitate the support solution. We use ErrorController that it was possible to improve the page 404 easier further. You also need to
make sure that the controller returns the code 404! public class ErrorController : MyController { #region Http404 public ActionResult Http404(string url) { Response.StatusCode = (int)HttpStatusCode.NotFound; var model = new NotFoundViewModel(); // ('NotFound' route), model.RequestedUrl = Request.Url.OriginalString.Contains(url) & Request.Url.OriginalString != url ? Request.Url.OriginalString : url; // Referrer Request model.ReferrerUrl = Request.UrlReferrer != null && Request.UrlReferrer.OriginalString != model.RequestedUrl ? Request.UrlReferrer.OriginalString : null; // TODO: ILogger return View("NotFound", model); } public class NotFoundViewModel { public string RequestedUrl { get; set; } public string ReferrerUrl { get; set; } } #endregion }
Step 2: Use our own base class for controllers to more easily call the method for 404 error and handle the HandleUnknownAction
Error 404 in ASP.NET MVC should be processed in several places. The first is HandleUnknownAction.
The InvokeHttp404 method is the only place to redirect to ErrorController and our newly created Http404 action. Use the
DRY !
public abstract class MyController : Controller { #region Http404 handling protected override void HandleUnknownAction(string actionName) { // - ErrorController, if (this.GetType() != typeof(ErrorController)) this.InvokeHttp404(HttpContext); } public ActionResult InvokeHttp404(HttpContextBase httpContext) { IController errorController = ObjectFactory.GetInstance<ErrorController>(); var errorRoute = new RouteData(); errorRoute.Values.Add("controller", "Error"); errorRoute.Values.Add("action", "Http404"); errorRoute.Values.Add("url", httpContext.Request.Url.OriginalString); errorController.Execute(new RequestContext( httpContext, errorRoute)); return new EmptyResult(); } #endregion }
Step 3: Use dependency injection in the controller factory and handle 404 HttpException
For example, so (it is not necessary to use
StructureMap ):
Example for MVC1.0: public class StructureMapControllerFactory : DefaultControllerFactory { protected override IController GetControllerInstance(Type controllerType) { try { if (controllerType == null) return base.GetControllerInstance(controllerType); } catch (HttpException ex) { if (ex.GetHttpCode() == (int)HttpStatusCode.NotFound) { IController errorController = ObjectFactory.GetInstance<ErrorController>(); ((ErrorController)errorController).InvokeHttp404(RequestContext.HttpContext); return errorController; } else throw ex; } return ObjectFactory.GetInstance(controllerType) as Controller; } }
Example for MVC2.0: protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { try { if (controllerType == null) return base.GetControllerInstance(requestContext, controllerType); } catch (HttpException ex) { if (ex.GetHttpCode() == 404) { IController errorController = ObjectFactory.GetInstance<ErrorController>(); ((ErrorController)errorController).InvokeHttp404(requestContext.HttpContext); return errorController; } else throw ex; } return ObjectFactory.GetInstance(controllerType) as Controller; }
I think it is better to catch errors in the place of their origin. Therefore, I prefer the method described above to error handling in Application_Error.
This is the second place to catch 404 errors.
Step 4: Add the NotFound route to Global.asax for paths that could not be determined by our application.
This route should trigger the action
Http404
. Note that the
url
parameter will be a relative address, because the routing engine cuts off the part with the domain name. That is why we added all these conditional statements in the first step.
routes.MapRoute("NotFound", "{*url}", new { controller = "Error", action = "Http404" });
This is the third and last place in the MVC application for catching 404 errors that you do not call yourself. If here it was not possible to match the incoming path to any controller and action, then MVC will transfer the processing of this error to the ASP.NET platform (to the Global.asax file). And we do not want this to happen.
Step 5: Finally, cause a 404 error when the application cannot find anything.
For example, when an incorrect ID parameter is passed to our Loan controller, inherited from MyController:
It would be great if you could implement all this with less code. But I think that this solution is easier to maintain, test, and in general it is more convenient.
Library for the second solution
And lastly: the library is already ready, allowing to organize error handling in the manner described above. You can find it here -
github.com/andrewdavey/NotFoundMvc .
Conclusion
For the sake of interest, I looked at how this problem was solved in
Orchard . I was surprised and somewhat disappointed - the developers decided not to handle this exception at all - their own 404 error pages, in my opinion, have long become a standard in web development.
In my application, I used error handling via Web.config using routing. Until the end of the application development, and to stop at handling the 404 error is quite dangerous - you can never release the application at all. Closer to the end, most likely, I will implement the second solution.
Related Links:
- ASP.NET, HTTP 404 and SEO .
- CustomErrors does not work when setting redirectMode = "ResponseRewrite" .
- How can I properly handle 404 in ASP.NET MVC?
- Error handling for the entire site in ASP.NET MVC 3 .
- ASP.NET MVC 404 Error Handling .
- HandleUnknownAction in ASP.NET MVC - Be Careful .
- github.com/andrewdavey/NotFoundMvc .