15

I have an MVC 4 app, using a custom HandleErrorAttribute to handle only custom exceptions. I would like to intercept the default 404 and other non-500 error pages and replace them with something more attractive. To that end, I added the following to my Web.config:

<system.web>
    <customErrors mode="On" defaultRedirect="~/Error/Index" />
...
</ system.web>

I have an Error controller with an Index method and corresponding view, but still I get the default 404 error page. I have also tried setting my defaultRedirect to a static html file to no avail. I have tried adding error handling specific to 404's inside <customErrors>, and I even tried modifying the routes programattically, all with no results. What am I missing? Why is ASP ignoring my default error handling?

Note: I noticed earlier that I cannot test my CustomHandleErrorAttribute locally, even with <customErrors mode="On". It does work when I hit it on my server from my dev box though... not sure if that is related. This guy had the same problem.

Community
  • 1
  • 1
therealmitchconnors
  • 2,732
  • 1
  • 18
  • 36
  • Do you have Fiddler installed? If yes the request you're making that should return 404, can you tell us what is it returning actually? – Marko Jul 14 '13 at 09:06
  • I don't think fiddler will tell me much. I want my custom exception page, but I'm getting the default .net 404. see http://therealmitchconnors-nat.azurewebsites.net/ggg (a non-existent page) for an example. – therealmitchconnors Jul 14 '13 at 16:35
  • What does your CustomHandlerAttribute code looks like? With me it works, only I don't have implemented any custom attributes for handling errors. – Andrew Jul 22 '13 at 14:49
  • In MVC it's actually advised to use the `HandleErrorAttribute` instead see http://stackoverflow.com/a/16430249/200442 for an example. – Daniel Little Jul 23 '13 at 03:25
  • Well, it looks like no one could really duplicate this bug, so the bounty goes to the first person to recommend using Application_Error, because it worked, even if it didn't answer my question. Thanks, everyone, for your input. – therealmitchconnors Jul 23 '13 at 19:26
  • There are many detailed post about custom error handling and error handling in general on Stackoverflow and other sites: http://stackoverflow.com/questions/619895/how-can-i-properly-handle-404-in-asp-net-mvc http://www.codeproject.com/Articles/422572/Exception-Handling-in-ASP-NET-MVC Additionally once you setup everything and you want to test your site for security you can use this site: https://asafaweb.com/ I suggest you read these and try to come up with something that best works for your scenario. – Marko Jul 14 '13 at 08:31
  • 1
    I have looked all over SO, MSDN, codeproject, etc. The solution always comes down to setting up `HandleErrorAttribute` for exceptions in your controllers (which is working), and to have all other exceptions (such as 404's caused by users hitting a bad address) handled using the customErrors' defaultRedirect attribute, which I have done. The problem is that it simply doesn't work. I still get the default ASP error page for things not handled by HandleError. – therealmitchconnors Jul 14 '13 at 15:00
  • Possible duplicate of [How to make custom error pages work in ASP.NET MVC 4](http://stackoverflow.com/questions/13905164/how-to-make-custom-error-pages-work-in-asp-net-mvc-4) – Liam Oct 10 '16 at 14:03

6 Answers6

14

This should work :

1. Web.Config

<customErrors mode="On"
   defaultRedirect="~/Views/Shared/Error.cshtml">

  <error statusCode="403"
    redirect="~/Views/Shared/UnauthorizedAccess.cshtml" />

  <error statusCode="404"
    redirect="~/Views/Shared/FileNotFound.cshtml" />

</customErrors>

2. Registered HandleErrorAttribute as a global action filter in the FilterConfig class as follows

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new CustomHandleErrorAttribute());
        filters.Add(new AuthorizeAttribute());
    }

If that dont work then, Try to make yourself transfer the response by checking status codes like the Following in the Global.asax: at least it must work.

void Application_EndRequest(object sender, EventArgs e)
{
    if (Response.StatusCode == 401)
    {
        Response.ClearContent();
        Server.Transfer("~/Views/Shared/UnauthorizedAccess.cshtml");
    }
}
N Subedi
  • 2,858
  • 2
  • 22
  • 36
  • 1
    This should work, though I would use the Application_Error event instead of EndRequest, but I would still prefer to solve this problem by getting the customErrors feature to work. If no answers are able to help with that by the end of the bounty, I will accept your answer. – therealmitchconnors Jul 18 '13 at 21:24
6

I am going little off topic. I thought this is bit important to explain.

enter image description here

If you pay attention to the above highlighted part. I have specified the order of the Action Filter. This basically describes the order of execution of Action Filter. This is a situation when you have multiple Action Filters implemented over Controller/Action Method

enter image description here

This picture just indicates that let's say you have two Action Filters. OnActionExecution will start to execute on Priority and OnActionExecuted will start from bottom to Top. That means in case of OnActionExecuted Action Filter having highest order will execute first and in case of OnActionExecuting Action Filter having lowest order will execute first. Example below.

public class Filter1 : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {

//Execution will start here - 1

        base.OnActionExecuting(filterContext);
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {

//Execution will move here - 5

        base.OnActionExecuted(filterContext);
    }
}

public class Filter2 : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {

//Execution will move here - 2

        base.OnActionExecuting(filterContext);
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {

//Execution will move here - 4

        base.OnActionExecuted(filterContext);
    }
}

[HandleError]
public class HomeController : Controller
{
    [Filter1(Order = 1)]
    [Filter2(Order = 2)]
    public ActionResult Index()
    {

//Execution will move here - 3

        ViewData["Message"] = "Welcome to ASP.NET MVC!";

        return View();
    }
}

You may already aware that there are different types of filters within MVC framework. They are listed below.

  1. Authorization filters

  2. Action filters

  3. Response/Result filters

  4. Exception filters

Within each filter, you can specify the Order property. This basically describes the order of execution of the Action Filters.

Back to the original Query

This works for me. This is very easy and no need to consider any change in Web.Config or Register the Action Filter in Global.asax file.

ok. So, First I am creating a simple Action Filter. This will handle Ajax and Non Ajax requests.

public class MyCustomErrorAttribute : HandleErrorAttribute
{
    public override void OnException(ExceptionContext filterContext)
    {
        filterContext.ExceptionHandled = true;
        var debugModeMsg = filterContext.HttpContext.IsDebuggingEnabled
                               ? filterContext.Exception.Message +
                                 "\n" +
                                 filterContext.Exception.StackTrace
                               : "Your error message";

//This is the case when you need to handle Ajax requests

        if (filterContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.Result = new JsonResult
            {
                JsonRequestBehavior = JsonRequestBehavior.AllowGet,
                Data = new
                {
                    error = true,
                    message = debugModeMsg
                }
            };
        }

//This is the case when you handle Non Ajax request

        else
        {
            var routeData = new RouteData();
            routeData.Values["controller"] = "Error";
            routeData.Values["action"] = "Error";
            routeData.DataTokens["area"] = "app";
            routeData.Values["exception"] = debugModeMsg;
            IController errorsController = new ErrorController();
            var exception = HttpContext.Current.Server.GetLastError();
            var httpException = exception as HttpException;
            if (httpException != null)
            {
                Response.StatusCode = httpException.GetHttpCode();
                switch (System.Web.HttpContext.Current.Response.StatusCode)
                {
                    case 404:
                        routeData.Values["action"] = "Http404";
                        break;
                }
            }

            var rc = new RequestContext
                         (
                             new HttpContextWrapper(HttpContext.Current),
                             routeData
                         );
            errorsController.Execute(rc);
        }
        base.OnException(filterContext);
    }
}

Now you can implement this Action Filter on Controller as well as on the Action only.Example:

enter image description here

Hope this should help you.

Imad Alazani
  • 6,688
  • 7
  • 36
  • 58
  • I think writing the Error Condition and redirect to is far better in Application_Error in Global.ajax. You dont have to write anywhere anything. What about that @PKKG? what do you think. If this follows standards? Plz i want a Suggestion. By looking your example i should create an Filter and I should use that filter everywhere. But, i found it is easier to implement just a single Procedure and nowhere else to write any bit of codes. Is this violate any .NET rules or whatever? – N Subedi Jul 22 '13 at 04:39
  • Is my approach flexible? Do you want to apply this setting to specific Action method/Specific Controller/Multiple Controller ? Do you want to apply this setting after some specific Action filter executed? Do you want to apply this setting after some specific Action method executed? What else do you want ? – Imad Alazani Jul 22 '13 at 04:49
  • This is very thorough, but the HandleErrorAttribute cannot intercept 404's, etc... – therealmitchconnors Jul 23 '13 at 19:27
  • @therealmitchconnors : You were right. 404 tracking is missing in this code. – Imad Alazani Jul 26 '13 at 05:14
2

I want to share my knowledge after investigating this problem. Any comments that help improve my statements are welcomed.

In ASP.NET MVC, there are three layers that handle HTTP requests in the following order (response is transferred in reverse order):

  1. IIS (HTTP Layer)

  2. ASP.NET (Server Layer)

  3. Controller (MVC Layer)

All of these layers have error handling, but each layer does it differently. I'll start with IIS.

IIS Layer

The simplest example of how IIS handles an error is to request a non existing .html file from your server, using the browser. The address should look like:

http://localhost:50123/this_does_not_exist.html

Notice the title of the browser tab, for example: IIS 10.0 Detailed Error - 404.0 - Not Found.

ASP.NET Layer

When IIS receives a HTTP request, if the URL ends with .aspx, it forwards it to ASP.NET since it is registered to handle this extension. The simplest example of how ASP.NET handles an error is to request a non existing .aspx file from your server, using the browser. The address should look like:

http://localhost:50123/this_does_not_exist.aspx

Notice the Version Information displayed at the bottom of the page, indicating the version of ASP.NET.

The customErrors tag was originally created for ASP.NET. It has effect only when the response is created by ASP.NET internal code. This means that it does not affect responses created from application code. In addition, if the response returned by ASP.NET has no content and has an error status code (4xx or 5xx), then IIS will replace the response according to the status code. I'll provide some examples.

If the Page_Load method contains Response.StatusCode = 404, then the content is displayed normally. If additional code Response.SuppressContent = true is added, then IIS intervenes and handles 404 error in the same way as when requesting "this_does_not_exist.html". An ASP.NET response with no content and status code 2xx is not affected.

When ASP.NET is unable to complete the request using application code, it will handle it using internal code. See the following examples.

If an URL cannot be resolved, ASP.NET generates a response by itself. By default, it creates a 404 response with HTML body containing details about the problem. The customErrors can be used to create a 302 (Redirect) response instead. However, accessing a valid URL that returns 404 response from application code does not trigger the redirect specified by customErrors.

The same happens when ASP.NET catches an exception from application code. By default, it creates a 500 response with HTML body containing details about the source code that caused the exception. Again, the customErrors can be used to generate a 302 (Redirect) response instead. However, creating a 500 response from application code does not trigger the redirect specified by customErrors.

The defaultRedirect and error tags are pretty straight-forth to understand considering what I just said. The error tag is used to specify a redirect for a specific status code. If there is no corresponding error tag, then the defaultRedirect will be used. The redirect URL can point to anything that the server can handle, including controller action.

MVC Layer

With ASP.NET MVC things get more complicated. Firstly, there may be two "Web.config" files, one in the root and one in the Views folder. I want to note that the default "Web.config" from Views does two things of interest to this thread:

  • It disables handling URLs to .cshtml files (webpages:Enabled set to false)
  • It prevents direct access to any content inside the Views folder (BlockViewHandler)

In the case of ASP.NET MVC, the HandleErrorAttribute may be added to GlobalFilters, which also takes into account the value of mode attribute of the customErrors tag from the root "Web.config". More specifically, when the setting is On, it enables error handling at MVC Layer for uncaught exceptions in controller/action code. Rather than forwarding them to ASP.NET, it renders Views/Shared/Error.cshtml by default. This can be changed by setting the View property of HandleErrorAttribute.

Error handling at MVC Layer starts after the controller/action is resolved, based on the Request URL. For example, a request that doesn't fulfill the action's parameters is handled at MVC Layer. However, if a POST request has no matching controller/action that can handle POST, then the error is handled at ASP.NET Layer.

I have used ASP.NET MVC 5 for testing. There seems to be no difference between IIS and IIS Express regarding error handling.

Answer

The only reason I could think of why customErrors is not considered is because they are created with HttpStatusCodeResponse from application code. In this case, the response is not altered by ASP.NET or IIS. At this point configuring an alternative page is pointless. Here is an example code that reproduces this behavior:

public ActionResult Unhandled404Error()
{
    return new HttpStatusCodeResult(HttpStatusCode.NotFound);
}

In such scenario, I recommend implementing an ActionFilterAttribute that will override OnResultExecuted and do something like the following:

int statusCode = filterContext.HttpContext.Response.StatusCode;
if(statusCode >= 400)
{
    filterContext.HttpContext.Response.Clear();
    filterContext.HttpContext.Response.Redirect("/Home/Index");
}

The implemented ActionFilterAttribute should be added to GlobalFilters.

MatrixRonny
  • 437
  • 3
  • 11
1

To me, it works deleting the default Error.cshtml file, now it is taking the custom Error defaultRedirect page in Web.config.

  • Same, but it was a bit more confusing in my case because I was actually trying to create a test to watch how different scenarios would be handled and I named my action Error, and subsequently my view, Error.cshtml. It kept showing my Error.cshtml view instead of redirecting to my customer page. Not sure whether Microsoft would call that a bug or a feature. To be clear, my Error.cshtml file was in my TestController so I had no idea it would pick it up there. – BVernon May 23 '23 at 14:18
0

Create a Controller ErrorController.

 public class ErrorController : Controller
    {
        //
        // GET: /Error/

        public ActionResult Index()
        {
            return View();
        }
}

Create the Index view for the action.

in Web.config

<customErrors mode="On">
      <error statusCode="404" redirect="Error/Index"/>
</customErrors>

When you are handling errors in your code/logic

[HandleError]
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            ViewBag.Message = "Modify this template to jump-start application.";

            return View("Index2");
        }
}

[HandleError] attribute - will redirected to the Error.cshtml page inside shared folder.

chamara
  • 12,649
  • 32
  • 134
  • 210
  • The problem is that HandleError only handles HTTP500 errors, which means only exceptions thrown inside your controller (and that part is working fine). I need to handle a user trying to hit a random page that doesn't exist in my site... right now it just gives ASP's default ugly 404 page. – therealmitchconnors Jul 14 '13 at 14:56
0

I am not sure this answer will help you but this a simple way... I placed error.html in / and turned mode to on for custom errors in web config and this works perfectly...

  <system.web>
    <customErrors defaultRedirect="~/Error.html" mode="On" />
  </system.web>

this error.html is a basic html page with head and body..

Venkata K. C. Tata
  • 5,539
  • 4
  • 22
  • 35