3

I am attempting to "catch all" 500 and 404 errors within my MVC application and I can't seem to grasp what is needed, even after reading all the articles and questions out there.

Web.config (this allows 500 errors to go to ~/Views/Shared/Error.cshtml):

<system.web>
    <customErrors mode="On" redirectMode="ResponseRewrite" />
</system.web>

I've setup the HomeController to throw an error to test the above setup:

public ActionResult Index()
{
    //Testing errors
    throw new Exception("Exception");

    return View();
}

In my Global.asax.cs, I have the following to log the 500 errors:

protected void Application_Error()
{
    var ex = Server.GetLastError();

    //Custom ExceptionLog
    new ExceptionLogHelper().Add("Application_Error", Response.Status, ex);
}

Now for the 404 errors:

In my RouteConfig.cs, I have the following routes, but can't seem to catch all 404s:

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            name: "Error",
            url: "Error/{code}",
            defaults: new { controller = "Error", action = "Index", code = UrlParameter.Optional }
        );

        //routes.MapRoute(
        //  name: "Controllers",
        //  url: "{controller}/{action}/{id}",
        //  defaults: new { controller = "Error", action = "Index", code = UrlParameter.Optional }
        //);

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );

        //Keep at bottom
        routes.MapRoute("CatchAll", "{*url}", new { controller = "Error", action = "Index", name = "no-route-found", code = "404" });

    }
}

CatchAll at the bottom is doing a good job catching everything that does not match the preceeding routes.

I have a bunch more test scenarios, but one that is bugging me is the following UrlParameter:

http://localhost:64275/does/not/exist/

The above URL is essentially http://localhost:64275/{controller}/{action}/{id}

I don't have a Controller named does, and I thought that defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } would default to the Home controller with Action of Index if no Controller was matched.

Another example that works:

http://localhost:64275/a/a/a/a/ (because it has 4 parts, not 3 or less)

Can someone explain where I might be going wrong? ...and what I am not understanding?

Should I be implementing something like this: .Net MVC Routing Catchall not working (Darin Dimitrov's answer)

protected void Application_Error(object sender, EventArgs e)
{
    Exception exception = Server.GetLastError();
    HttpException httpException = exception as HttpException;
    if (httpException != null)
    {
        RouteData routeData = new RouteData();
        routeData.Values.Add("controller", "Error");
        routeData.Values.Add("action", "HttpError500");

            if (httpException.GetHttpCode() == 404)
            {
                routeData.Values["action"] = "HttpError404";
            }

        Server.ClearError();
        Response.Clear();
        IController errorController = new ErrorController();
        errorController.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
    }
}
NightOwl888
  • 55,572
  • 24
  • 139
  • 212
Derek
  • 653
  • 7
  • 20
  • `and I thought that` You thought wrong. It doesn't work like that. It matched the pattern, so it tries to use the `does` controller. It doesn't factor whether or not `DoesController` exists in the decision making process. – mjwills Feb 14 '18 at 02:37

1 Answers1

2

You are correct in that the default route is being hit in the http://localhost:64275/does/not/exist/ case. But, routing doesn't have any expensive Reflection calls built in to ensure the controller exists before attempting to create it.

However, you can intervene by making your own custom IControllerFactory that knows what to do when MVC can't locate the controller instance that is specified in the route.

NotFoundControllerFactory.cs

public class NotFoundControllerFactory : DefaultControllerFactory
{
    protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
    {
        // If controller not found it will be null, so we want to take control
        // of the request and send it to the ErrorController.NotFound method.
        if (controllerType == null)
        {
            requestContext.RouteData.Values["action"] = "NotFound";
            requestContext.RouteData.Values["controller"] = "Error";
            return base.GetControllerInstance(requestContext, typeof(ErrorController));
        }

        return base.GetControllerInstance(requestContext, controllerType);
    }
}

DefaultControllerFactory accepts the controller as a string and then attempts to resolve the controller name to a type. If it can't the type is null when it calls GetControllerInstance(RequestContext, Type). So, all we have to do is check whether the type is null and then rewrite our request so it instantiates the ErrorController and calls the NotFound action method.

Usage

You just need to register the controller factory with MVC at startup.

ControllerBuilder.Current.SetControllerFactory(new NotFoundControllerFactory());

Then to tie everything together, your NotFound action method should set the status code to 404.

public class ErrorController : Controller
{
    public ActionResult NotFound()
    {
        Response.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
        Response.StatusDescription = "404 Not Found";

        return View();
    }
}

As for action methods, MVC does respond with a 404 Not Found if the action does not exist on a controller.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • Thanks for the suggestion, I've had limited testing with this, but I believe this will work as a clean solution for my project! – Derek Feb 14 '18 at 19:14