How can I properly handle 404 in ASP.NET MVC?

I am just getting started on ASP.NET MVC so bear with me. I've searched around this site and various others and have seen a few implementations of this.

EDIT: I forgot to mention I am using RC2

Using URL Routing:

routes.MapRoute(
    "Error",
     "{*url}",
     new { controller = "Errors", action = "NotFound" }  // 404s
);

The above seems to take care of requests like this (assuming default route tables setup by initial MVC project): "/blah/blah/blah/blah"

Overriding HandleUnknownAction() in the controller itself:

// 404s - handle here (bad action requested
protected override void HandleUnknownAction(string actionName) {
    ViewData["actionName"] = actionName;
    View("NotFound").ExecuteResult(this.ControllerContext);
}  

However the previous strategies do not handle a request to a Bad/Unknown controller. For example, I do not have a "/IDoNotExist", if I request this I get the generic 404 page from the web server and not my 404 if I use routing + override.

So finally, my question is: Is there any way to catch this type of request using a route or something else in the MVC framework itself?

OR should I just default to using Web.Config customErrors as my 404 handler and forget all this? I assume if I go with customErrors I'll have to store the generic 404 page outside of /Views due to the Web.Config restrictions on direct access. Anyway any best practices or guidance is appreciated.


The code is taken from http://blogs.microsoft.co.il/blogs/shay/archive/2009/03/06/real-world-error-hadnling-in-asp-net-mvc-rc2.aspx and works in ASP.net MVC 1.0 as well

Here's how I handle http exceptions:

protected void Application_Error(object sender, EventArgs e)
{
   Exception exception = Server.GetLastError();
   // Log the exception.

   ILogger logger = Container.Resolve<ILogger>();
   logger.Error(exception);

   Response.Clear();

   HttpException httpException = exception as HttpException;

   RouteData routeData = new RouteData();
   routeData.Values.Add("controller", "Error");

   if (httpException == null)
   {
       routeData.Values.Add("action", "Index");
   }
   else //It's an Http Exception, Let's handle it.
   {
       switch (httpException.GetHttpCode())
       {
          case 404:
              // Page not found.
              routeData.Values.Add("action", "HttpError404");
              break;
          case 500:
              // Server error.
              routeData.Values.Add("action", "HttpError500");
              break;

           // Here you can handle Views to other error codes.
           // I choose a General error template  
           default:
              routeData.Values.Add("action", "General");
              break;
      }
  }           

  // Pass exception details to the target error View.
  routeData.Values.Add("error", exception);

  // Clear the error on server.
  Server.ClearError();

  // Avoid IIS7 getting in the middle
  Response.TrySkipIisCustomErrors = true; 

  // Call target Controller and pass the routeData.
  IController errorController = new ErrorController();
  errorController.Execute(new RequestContext(    
       new HttpContextWrapper(Context), routeData));
}

Requirements for 404

The following are my requirements for a 404 solution and below i show how i implement it:

  • I want to handle matched routes with bad actions
  • I want to handle matched routes with bad controllers
  • I want to handle un-matched routes (arbitrary urls that my app can't understand) - i don't want these bubbling up to the Global.asax or IIS because then i can't redirect back into my MVC app properly
  • I want a way to handle in the same manner as above, custom 404s - like when an ID is submitted for an object that does not exist (maybe deleted)
  • I want all my 404s to return an MVC view (not a static page) to which i can pump more data later if necessary (good 404 designs) and they must return the HTTP 404 status code
  • Solution

    I think you should save Application_Error in the Global.asax for higher things, like unhandled exceptions and logging (like Shay Jacoby's answer shows) but not 404 handling. This is why my suggestion keeps the 404 stuff out of the Global.asax file.

    Step 1: Have a common place for 404-error logic

    This is a good idea for maintainability. Use an ErrorController so that future improvements to your well designed 404 page can adapt easily. Also, make sure your response has the 404 code !

    public class ErrorController : MyController
    {
        #region Http404
    
        public ActionResult Http404(string url)
        {
            Response.StatusCode = (int)HttpStatusCode.NotFound;
            var model = new NotFoundViewModel();
            // If the url is relative ('NotFound' route) then replace with Requested path
            model.RequestedUrl = Request.Url.OriginalString.Contains(url) & Request.Url.OriginalString != url ?
                Request.Url.OriginalString : url;
            // Dont get the user stuck in a 'retry loop' by
            // allowing the Referrer to be the same as the Request
            model.ReferrerUrl = Request.UrlReferrer != null &&
                Request.UrlReferrer.OriginalString != model.RequestedUrl ?
                Request.UrlReferrer.OriginalString : null;
    
            // TODO: insert ILogger here
    
            return View("NotFound", model);
        }
        public class NotFoundViewModel
        {
            public string RequestedUrl { get; set; }
            public string ReferrerUrl { get; set; }
        }
    
        #endregion
    }
    

    Step 2: Use a base Controller class so you can easily invoke your custom 404 action and wire up HandleUnknownAction

    404s in ASP.NET MVC need to be caught at a number of places. The first is HandleUnknownAction .

    The InvokeHttp404 method creates a common place for re-routing to the ErrorController and our new Http404 action. Think DRY!

    public abstract class MyController : Controller
    {
        #region Http404 handling
    
        protected override void HandleUnknownAction(string actionName)
        {
            // If controller is ErrorController dont 'nest' exceptions
            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 your Controller Factory and wire up 404 HttpExceptions

    Like so (it doesn't have to be StructureMap):

    MVC1.0 example:

    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;
        }
    }
    

    MVC2.0 example:

        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 its better to catch errors closer to where they originate. This is why i prefer the above to the Application_Error handler.

    This is the second place to catch 404s.

    Step 4: Add a NotFound route to Global.asax for urls that fail to be parsed into your app

    This route should point to our Http404 action. Notice the url param will be a relative url because the routing engine is stripping the domain part here? That is why we have all that conditional url logic in Step 1.

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

    This is the third and final place to catch 404s in an MVC app that you don't invoke yourself. If you don't catch unmatched routes here then MVC will pass the problem up to ASP.NET (Global.asax) and you don't really want that in this situation.

    Step 5: Finally, invoke 404s when your app can't find something

    Like when a bad ID is submitted to my Loans controller (derives 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 nice if all this could be hooked up in fewer places with less code but i think this solution is more maintainable, more testable and fairly pragmatic.

    Thanks for the feedback so far. I'd love to get more.

    NOTE: This has been edited significantly from my original answer but the purpose/requirements are the same - this is why i have not added a new answer


    ASP.NET MVC doesn't support custom 404 pages very well. Custom controller factory, catch-all route, base controller class with HandleUnknownAction - argh!

    IIS custom error pages are better alternative so far:

    web.config

    <system.webServer>
      <httpErrors errorMode="Custom" existingResponse="Replace">
        <remove statusCode="404" />
        <error statusCode="404" responseMode="ExecuteURL" path="/Error/PageNotFound" />
      </httpErrors>
    </system.webServer>
    

    ErrorController

    public class ErrorController : Controller
    {
        public ActionResult PageNotFound()
        {
            Response.StatusCode = 404;
            return View();
        }
    }
    

    Sample Project

  • Test404 on GitHub
  • Live website
  • 链接地址: http://www.djcxy.com/p/20542.html

    上一篇: 在ASP.NET MVC中将文件返回到View / Download

    下一篇: 我如何正确处理ASP.NET MVC中的404?