How to use CustomErrors in ASP.NET MVC 2

First, let’s see how our Web.config file will look like and after that I will show you how to create the pages.

<customErrors mode="On" defaultRedirect="~/Error">
  <error statusCode="404" redirect="~/Error/NotFound"/>
  <error statusCode="403" redirect="~/Error/AccessDenied"/>
</customErrors>

I’m going to show the steps to make it work for those three types of errors and also explain you about the HandleError attribute and how it handles the information specified in the customErrors. I’m also putting here the code I wrote in order to handle the AccessDenied error.

Error Page

When we are talking about the Error Page, one thing that you should understand is that without the HandleError Attribute, if an error occurs the .NET will just redirect the user to the page you specified in your Web.Config. So, if you have the “defaultRedirect” property pointing to something like “~/GenericError.aspx”, the GenericError.aspx page under the root folder of your project will be shown without any information about the Error. It could be a static HTML page that only tells the user “Hey, you’ve got an Error”.

Now, if you what you want is to show the user some information about the error, like the description or even the stackTrace but with the page looking like your default theme, then you can use the HandleError Attribute. By applying it to a Controller or a method the behavior of the customErrors changes a little bit. Instead of just pointing to some page, the exception handling will render a View, more specifically, a view called Error (if you don’t specify any other name to the Attribute) it doesn’t matter what name you’ve told the “defaultRedirect” to take you to. Along with that, it will send a HandleErrorInfo model containing the Controller name where the exception occurred, the Action name and the exception itself, so you can get the stackTrace and the description of the error.

Although it might seem obvious for some what the HandleError attribute does I’ve seen it’s a common issue people setting their customErrors to show something like GenericError.aspx and using the HandleError attribute. Or even, setting the defaultRedirect property to “Error” and expecting the Index method within the ErrorController to receive a HandleErrorInfo parameter. Remember, when using the HandleError attribute, the “Error“ View will be rendered internally, without even getting into the ErrorController.

Common mistake

Web.Config

<customErrors mode="On" defaultRedirect="Error/Ops" />

HomeController.cs

[HandleError] // using HandleError attribute
public class HomeController : Controller
{
    public ActionResult Index()
    {
		return View();
    }
}

ErrorController.cs

public class ErrorController : Controller
{
    public ActionResult Ops()
    {
        //
		// Wrong! It won't get here because
		// the HandleError will handle the error.
		return View();
    }
}

The right way

I’m going to demonstrate now how you can make it work with a page that shows some information about the error. With this example, even though someone forget using the HandleError attribute, a friendly page still shows up but without the error info (it’s better than not showing anything). It could be improved, off course, but that it’s another point.

Web.config

<customErrors mode="On" defaultRedirect="Error" />

Error.aspx

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<System.Web.Mvc.HandleErrorInfo>" %>

<asp:Content ID="errorTitle" ContentPlaceHolderID="TitleContent" runat="server">
    Error
</asp:Content>

<asp:Content ID="errorContent" ContentPlaceHolderID="MainContent" runat="server">       

    <h2>
        Sorry, an error occurred while processing your request.
    </h2>

	<% if (Model.Exception != null ) { %>
		<p>
		  Controller: <%= Model.ControllerName %>
		</p>
		<p>
		  Action: <%= Model.ActionName %>
		</p>
		<p>
		  Message: <%= Model.Exception.Message%>
		</p>
		<p>
		  Stack Trace: <%= Model.Exception.StackTrace%>
		</p>
	<% } %>
</asp:Content>

Place that page inside your Views folder. It can be either your Shared or Error folder. The name must be Error.aspx.

That’s it! Only the aspx file and the setting inside the Web.config.

Only if you want that whether the Controller have or not the HandleError attribute, your custom error page will still be shown, then you can add an action method into your ErrorController:

ErrorController.cs

public class ErrorController : Controller
{
    public ActionResult Index()
    {
		return View("Error");
    }
}

Now whether the page is being redirected to the defaultRedirect or rendered by the HandleError, the page will always be called.

To force an error, let’s create a code that throws an exception inside the Index of the HomeController:

int a = 0;
int b = 0;
int result = a / b;

That will throw the DivideByZeroException, see the friendly page in action:

CustomErrros Error Handling

 

404 NotFound Page

For the Not Found page we don’t have much to tell the user, so there is not much to do. It’s only the redirect to a page and, the information we can show is the wrong path. That information already comes as a query string.

Just to centralize the error handling and make it more organized, let’s put our NotFound within our ErrorController. So, add your the following method to the ErrorController:

public ActionResult NotFound(string aspxerrorpath)
{
	ViewData["error_path"] = aspxerrorpath;

	return View();
}

and the aspx page code:

NotFound.aspx

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="errorTitle" ContentPlaceHolderID="TitleContent" runat="server">
    Not Found
</asp:Content>

<asp:Content ID="errorContent" ContentPlaceHolderID="MainContent" runat="server">
    <h2>
        Sorry, the page <%: ViewData["error_path"] %> does not exist.
    </h2>    
</asp:Content>

and add the following line for the Not Found error into your customErrors section:

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

See the result:CustomError Page Not Found

 

403 AccessDenied

Most of the times when someone doesn’t have access to a page it’s only a login issue. But there are also times that a user has logged in but they are allowed to see something or perform some task, maybe because the lack of sufficient privileges. I’m going to show here how to handle those type of errors and the way I do that is by creating an ActionFilter attribute that you can put on a class or a method.

I’m going to hide some code for clarity:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Configuration;
using System.Web.Configuration;

namespace ErrorHandling.Controllers
{
    public class AuthorizationAttribute : ActionFilterAttribute, IExceptionFilter
    {
        public string Action { get; set; }
        
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            //
            // check permissions

            if (accessDenied)
            {
                throw new System.Web.HttpException((int)System.Net.HttpStatusCode.Forbidden,
                        "You are now allowed to see this page.");
            }

            base.OnActionExecuting(filterContext);
        }

        #region IExceptionFilter Members

        // this method is almost a clone of HandleErrorAttribute from MVC
        // it's just changed so that we take the error message to the page set in the <customErrors>
        public virtual void OnException(ExceptionContext filterContext)
        {
            if (filterContext == null)
            {
                throw new ArgumentNullException("filterContext");
            }
            if (filterContext.IsChildAction)
            {
                return;
            }

            // If custom errors are disabled, we need to let the normal ASP.NET exception handler
            // execute so that the user can see useful debugging information.
            if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
                return;

            Exception exception = filterContext.Exception;

            // If this is not an HTTP 403 (for example, if somebody throws an HTTP 500 from an action method),
            // ignore it.
            if (new HttpException(null, exception).GetHttpCode() != 403)
            {
                return;
            }

            // try to get the customError page for the 403 code
            string customErrorPage = GetCustomError("403");

            // if there isn't a redirect to a 403 error page then get out
            if (customErrorPage == null)
                return;

            filterContext.Result = new RedirectResult(String.Concat(customErrorPage, "?action=", this.Action));
            filterContext.ExceptionHandled = true;
            filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;            
        }

        #endregion

        public string GetCustomError(string statusCode)
        {
            CustomErrorsSection customErrorsSection = ConfigurationManager.GetSection("system.web/customErrors") as CustomErrorsSection;

            if (customErrorsSection != null)
            {
                CustomError customErrorPage = customErrorsSection.Errors[statusCode];

                if (customErrorPage != null)
                    return customErrorPage.Redirect;
            }
            return null;
        }        
    }
}

What that code does is throw an 403 Exception if the user rights are not enough, then we implement the IExceptionFilter, so we can handle the error on our own way with the OnException method. Notice we are reading the customErrors section looking for the page that is set for the 403 code and redirecting the user to that page.

With that AuthorizationAttribute you can decorate any class or method that you want to be checked when a user access it, like so:

[AuthorizationAttribute]
[HandleError]
public class HomeController : Controller

Let’s create our page inside the Views folder (again, it can be the Shared or the Errors folder) to tell the user about the error:

AccessDenied.aspx

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="errorTitle" ContentPlaceHolderID="TitleContent" runat="server">
    Access Denied
</asp:Content>

<asp:Content ID="errorContent" ContentPlaceHolderID="MainContent" runat="server">
    <h2>
        Sorry, you do not have access to the action <%: ViewData["action"] %>.
    </h2>    
</asp:Content>

Add one more action method to our ErrorController:

public ActionResult AccessDenied(string action)
{
	ViewData["action"] = action;

	return View();
}

In this example, I’m passing in a parameter called “action” to the AccessDenied method, but it could be anything you want and can contain any information you want.

And finally, completing our Web.Config, we now add the treatment for the 403 error code:

<customErrors mode="On" defaultRedirect="Error">
  <error statusCode="404" redirect="Error/NotFound"/>
  <error statusCode="403" redirect="Error/AccessDenied"/>
</customErrors>

Here is how our ErrorController end up like:

namespace ErrorHandling.Controllers
{
    public class ErrorController : Controller
    {
        public ActionResult Index()
        {
            return View("Error");
        }

        public ActionResult NotFound(string aspxerrorpath)
        {
            ViewData["error_path"] = aspxerrorpath;

            return View();
        }

        public ActionResult AccessDenied(string action)
        {
            ViewData["action"] = action;

            return View();
        }
    }
}

With this structure the errors are very organized and all the things related to it are together.

About these ads

15 responses to “How to use CustomErrors in ASP.NET MVC 2

  1. Pingback: You can’t tell me what the error is because there was an error? ASP.NET MVC 2 and ELMAH « Mark Gilbert’s Blog

  2. Joe H

    In your code block you have:
    if(accessDenied)

    accessDenied is not valid. What is this supposed to be?

    • devstuffs

      Hey Joe,

      See the comments above the line if(accessDenied)? There is actually a place where you need to put your logic to check the permissions the way you want. You might want to check a Session, or the Identity object, or maybe the Database. The if statement is just to illustrate where to check it.

  3. How To Handle The View Exception

    Suppose when I render view and exeception error throw but can’t get source error message so how can handle view exception

    • devstuffs

      In order to render a view you have to call a Controller. Adding the HandleError attribute to that controller will send the error information (model HandleErrorInfo) to the view. Any unhandled error (like one in the view) will be caught and handled by the attribute.

      If you want a specific error handling to some controller, override the OnException method of that controller.

      For any other error handling there is always the Application_Error method in Global.asax

  4. Rob Seder

    Any idea how to get this working with MVC2? It doesn’t work the same way (out of the box) as you describe above. Any ideas?

    • devstuffs

      If you elaborate more what went wrong for you… :)

      • Rob Seder

        Do File -> New Project -> ASP.NET MVC 2 project

        Check

        In HomeController.Index add a “throw new NotImplementedException();”

        HomeController already has a [HandleError]

        You get a run-time exception telling you to set <customErrors mode="On… etc

        If I set the defaultRedirect="Error" – I get a 404 that ~/Error was not found.

        So in summary, it doesn't work the same way (out of the box) as you describe above. Any ideas how to get this working with MVC2? I don't have the option to upgrade to MVC3 and beyond. Thanks!

      • devstuffs

        I created a Gist for you with some pieces of my code (only the important parts) from a MVC website I have here.

        Here is the link https://gist.github.com/3874727

        I just didn’t put the Views in the Gist, but there’s nothing special with them.

      • Rob Seder

        I’ve run across that type of code too – I can’t find where GlobalFilterCollection comes from. The Internet tells me it’s in System.Web.Mvc – and I’m assuming MVC3 – because it’s not in MVC2.

        Any ideas on how to get this working with MVC2? MVC3 seems to be pretty well-documented with this approach, but it doesn’t appear to exist(?) in MVC2. Thanks again

      • devstuffs

        GlobalFilterCollection is not mandatory…

        If you are having trouble with it, just decorate your Controller or the Action you are trying to reach with [HandleError]

        That should trigger the ActionFilter to handle the error!

      • Rob Seder

        I believe ActionFilters were introduced in MVC3. I’m forced to work with MVC2.

        The [HandlerError] part works – it catches the error, to-where it gets redirected is the problem.

        gives me a configuration error at run-time
        gives me a 404 that ~/Error is not found

        And then if I create my own ErrorController and create an Index.aspx view – and set defaultRedirect=”~/Error/Index” – that just results in a blank page.

        Thanks anyhow for your time

      • devstuffs

        The Global Filter indeed were introduced in MVC 3, but it just removes the need of having to decorate all your classes with the HandleError attribute.

        I tried to explain in details everything you just told me you did, maybe I wasn’t that clear.

        If you have the Default MVC Route, it uses the Index Action when you don’t specify any, that means if you tell customErrors to redirect to Error, it should automatically understand that the Action is Index because that’s the default.

        You also need to have the ErrorController and the Views appropriately created.

        So, I assume you did it. That’s good!

  5. jose

    thanks for the help, Greetings

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 79 other followers

%d bloggers like this: