Native 404 error page on ASP.NET MVC

When developing a project on ASP.NET MVC, there was a need to make our own 404 error page. I expected that I could 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 below.

In ASP.NET MVC 3, which I work with, global filters have appeared. In the template of a new project, its use is already built-in to display its own error page (via HandleErrorAttribute ). A wonderful method, only it can not handle errors with the code 404 (page not found). Therefore, I had to look for another beautiful option for handling this error.

404 error handling via Web.config


The ASP.NET platform provides the ability to arbitrarily handle errors by using the simple configuration of the Web.config file. To do this, add the following code to the system.web section:

If a 404 error occurs, the user will be sent to the page yoursite.ru/Errors/Error404/?aspxerrorpath=/Not/Found/Url. In addition, it is not very nice and comfortable (you can not edit the url), so also bad for SEO - Article in habr.ru .
The method can be slightly improved by adding redirectMode = "ResponseRewrite" to customErrors:

In this case, the redirect to the error processing page should not occur, but the requested error path will be replaced with the contents of the specified page. However, there are some difficulties. On ASP.NET MVC this method in the given form does not work. A fairly detailed discussion (in English) can be read 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. With dynamic pages, as in the example, it refuses to work (since it does not see the file '/ Errors / Error404 /' 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 error handling actions, for example, logging.
In the specified topic, it was proposed to add the following code to each page:
Response.TrySkipIisCustomErrors = true;

This method only works with IIS 7 and higher, so this method could not be verified - we use IIS 6. We had to continue the search.

Dancing with a Tambourine and Application_Error


If the method described above cannot be applied for any reason, you will have to write more lines of code. A partial solution is given in the article .
The most complete solution "with a tambourine" I found in the topic . The discussion is in English, so I will translate the text of the decision into Russian.
Below are my requirements for solving the 404 NotFound error display problem:
  • I want to handle paths for which an action is not defined.
  • I want to handle paths for which a controller is not defined.
  • I want to handle paths that my application failed to parse. I do not want these errors to be handled 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) 404 errors in the same style.
  • I want all 404 errors to return an MVC View, not a static page, so that later 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 work with 404 error. Therefore, I try to put all the code associated with 404 error out of the Global file. asax.

Step 1: Create a common place to handle 404 error

This will facilitate the support decision. We use ErrorController to make it easier to improve the 404 page in the future. You also need to make sure that the controller returns the 404 code!
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: We use our own base class for controllers to make it easier to call the method for 404 error and handle HandleUnknownAction

Error 404 in ASP.NET MVC needs to be handled in several places. The first is HandleUnknownAction.
The InvokeHttp404 method is the one place to redirect to the ErrorController and our newly created Http404 action. Use the DRY methodology  !
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();
        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, like this (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).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).InvokeHttp404(requestContext.HttpContext);
            return errorController;
        }
        else
            throw ex;
    }
    return ObjectFactory.GetInstance(controllerType) as Controller;
}

I think it's better to catch errors at the place of their occurrence. 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 our application could not determine

This route should trigger an action Http404. Please note that the parameter  urlwill 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 cause yourself. If it was not possible to map the incoming path to any controller and action, MVC will pass the handling of this error on to the ASP.NET platform (in the Global.asax file). And we do not want this to happen.

Step 5: Finally, we cause error 404 when the application cannot find anything

For example, when an invalid ID parameter is passed to our Loan controller inherited from MyController:
//
// GET: /Detail/ID
public ActionResult Detail(int ID)
{
    Loan loan = this._svc.GetLoans().WithID(ID);
    if (loan == null)
        return this.InvokeHttp404(HttpContext);
    else
        return View(loan);
}

It would be great if you could implement all this with less code. But I believe that this solution is easier to maintain, test, and overall it is more convenient.

Library for the second solution


And lastly: a library is already ready that allows you to organize error handling in the manner described above. You can find her here -  github.com/andrewdavey/NotFoundMvc .

Conclusion


For fun, 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 the standard in web development.

In my application, I used error handling through Web.config using routing. Until the development of the application is completed, it’s quite dangerous to dwell on the handling of 404 errors - you can never release the application at all then. Closer to the end, most likely, I will introduce the second solution.

Related links:
  1. ASP.NET, HTTP 404 and SEO .
  2. CustomErrors does not work when setting redirectMode = "ResponseRewrite" .
  3. How can I properly handle 404 in ASP.NET MVC?
  4. Error handling for the entire site in ASP.NET MVC 3 .
  5. ASP.NET MVC 404 Error Handling .
  6. HandleUnknownAction in ASP.NET MVC - Be Careful .
  7. github.com/andrewdavey/NotFoundMvc .

Also popular now: