ASP.MVC HandleError attribute doesn't work

17,061

Solution 1

Over the years I have struggled to implement "handling custom errors" in ASP.NET MVC smoothly.

I have had successfully used Elmah before however was overwhelmed with the numerous cases that needs to be handled and tested differently (i.e. local vs IIS).

Recently in one of my project which is now live, I have used following approach (seems to be working fine in local and production env).

I do not specify customErrors or any settings in web.config at all.

I override Application_Error and handle all cases there, invoking specific actions in ErrorController.

I am sharing this if it helps and also to get a feedback (though things are working, you never know when it starts breaking ;))

Global.asax.cs

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();

        WebApiConfig.Register(GlobalConfiguration.Configuration);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        AuthConfig.RegisterAuth();
    }

    protected void Application_Error(object sender, EventArgs e)
    {
        System.Diagnostics.Trace.WriteLine("Enter - Application_Error");

        var httpContext = ((MvcApplication)sender).Context;

        var currentRouteData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext));
        var currentController = " ";
        var currentAction = " ";

        if (currentRouteData != null)
        {
            if (currentRouteData.Values["controller"] != null &&
                !String.IsNullOrEmpty(currentRouteData.Values["controller"].ToString()))
            {
                currentController = currentRouteData.Values["controller"].ToString();
            }

            if (currentRouteData.Values["action"] != null &&
                !String.IsNullOrEmpty(currentRouteData.Values["action"].ToString()))
            {
                currentAction = currentRouteData.Values["action"].ToString();
            }
        }

        var ex = Server.GetLastError();

        if (ex != null)
        {
            System.Diagnostics.Trace.WriteLine(ex.Message);

            if (ex.InnerException != null)
            {
                System.Diagnostics.Trace.WriteLine(ex.InnerException);
                System.Diagnostics.Trace.WriteLine(ex.InnerException.Message);
            }
        }

        var controller = new ErrorController();
        var routeData = new RouteData();
        var action = "CustomError";
        var statusCode = 500;

        if (ex is HttpException)
        {
            var httpEx = ex as HttpException;
            statusCode = httpEx.GetHttpCode();

            switch (httpEx.GetHttpCode())
            {
                case 400:
                    action = "BadRequest";
                    break;

                case 401:
                    action = "Unauthorized";
                    break;

                case 403:
                    action = "Forbidden";
                    break;

                case 404:
                    action = "PageNotFound";
                    break;

                case 500:
                    action = "CustomError";
                    break;

                default:
                    action = "CustomError";
                    break;
            }
        }
        else if (ex is AuthenticationException)
        {
            action = "Forbidden";
            statusCode = 403;
        }

        httpContext.ClearError();
        httpContext.Response.Clear();
        httpContext.Response.StatusCode = statusCode;
        httpContext.Response.TrySkipIisCustomErrors = true;
        routeData.Values["controller"] = "Error";
        routeData.Values["action"] = action;

        controller.ViewData.Model = new HandleErrorInfo(ex, currentController, currentAction);
        ((IController)controller).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
    }

}

ErrorController.cs

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

    public ActionResult CustomError()
    {
        Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        return View();
    }
}

This is all I have. No HandleErrorAttribute registered.

I found this approach less confusing and simple to extend. Hope this helps someone.

Solution 2

Setting customErrors to on should be enough to see the results locally.

<customErrors mode="On" />

As you are registering the HandleErrorAttribute globally you do not need to decorate your action method with it as it will be applied by default.

public class TestController : Controller
{
    public ActionResult Index()
    {
        throw new Exception("oops");
        return View();
    }
}

As long as you have registered the HandleErrorAttribute in filterConfig and that

FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

Is in Application_Start() in Global.asax.cs then it should work.

If you are going to create custom error pages I recommend reading this blog post

Mvc custom error pages

Solution 3

I use DI in almost all of my applications. Even if you do not use dependency injection - it is very useful for a global exception handler for MVC (Web API) applications.

I like @SBirthare's approach - but I would put it in a class that any IoC would resolve.

I prefer Autofac - but combining @SBirthare's technique with some DI should give you a centralized place to configure your exception handling - but also the ability to register different types of exception handling (if you needed it).

This is what I traditionally do:

public abstract class ExceptionHandlerService : IExceptionHandlerService
{
    ILoggingService _loggingSerivce;

    protected ExceptionHandlerService(ILoggingService loggingService)
    {
        //Doing this allows my IoC component to resolve whatever I have
        //configured to log "stuff"
        _loggingService = loggingService;


    }
    public virtual void HandleException(Exception exception)
    {


        //I use elmah a alot - and this can handle WebAPI 
       //or Task.Factory ()=> things where the context is null
        if (Elmah.ErrorSignal.FromCurrentContext() != null)
        {
            Elmah.ErrorSignal.FromCurrentContext().Raise(exception);
        }
        else
        {
            ErrorLog.GetDefault(null).Log(new Error(exception));
        }

        _loggingService.Log("something happened", exception)
    }
}

Now you need to register this

builder.RegisterType<ExceptionHandlerService>().As<IExceptionHandlerService();

In an MVC app - you need to implement a class that implements IExceptionFilter

public class CustomHandleError : IExceptionFilter
{
    private readonly IExceptionHandlerService _exceptionHandlerService;
    public CustomHandleError(IExceptionHandlerService exceptionHandlerService)
    {
        _exceptionHandlerService = exceptionHandlerService;
    }

    public void OnException(ExceptionContext filterContext)
    {
        _exceptionHandlerService.HandleException(filterContext.Exception);
    }
}

To register filters in Autofac

builder.Register(ctx => new CustomHandleError(ctx.Resolve<IExceptionHandlerService>())).AsExceptionFilterFor<BaseController>();

I always define a BaseController that all my other controllers derive from. You can define an authorization filter using the same technique. Now all controllers are secured and exception handled.

Now you don't need attributes on any classes - the code is in one spot.

I don't have any try catch's anywhere so we can preserve the stack trace by the time the exception is caught by the exception handler.

If you combine this technique with @SBirthare's -

public abstract class ExceptionHandlerService : IExceptionHandlerService
{
ILoggingService _loggingSerivce;

protected ExceptionHandlerService(ILoggingService loggingService)
{
    //Doing this allows my IoC component to resolve whatever I have
    //configured to log "stuff"
    _loggingService = loggingService;


}
public virtual void HandleException(Exception exception)
{


    //I use elmah a alot - and this can handle WebAPI 
   //or Task.Factory ()=> things where the context is null
    if (Elmah.ErrorSignal.FromCurrentContext() != null)
    {
        Elmah.ErrorSignal.FromCurrentContext().Raise(exception);
    }
    else
    {
        ErrorLog.GetDefault(null).Log(new Error(exception));
    }

    _loggingService.Log("something happened", exception)

    //re-direct appropriately
    var controller = new ErrorController();
    var routeData = new RouteData();
    var action = "CustomError";
    var statusCode = 500;

        statusCode = exception.GetHttpCode();

        switch (exception.GetHttpCode())
        {
            case 400:
                action = "BadRequest";
                break;

            case 401:
                action = "Unauthorized";
                break;

            case 403:
                action = "Forbidden";
                break;

            case 404:
                action = "PageNotFound";
                break;

            case 500:
                action = "CustomError";
                break;

            default:
                action = "CustomError";
                break;
         }
    //I didn't add the Authentication Error because that should be a separate filter that Autofac resolves.

   var httpContext = ((MvcApplication)sender).Context;
    httpContext.ClearError();
    httpContext.Response.Clear();
    httpContext.Response.StatusCode = statusCode;
    httpContext.Response.TrySkipIisCustomErrors = true;
    routeData.Values["controller"] = "Error";
    routeData.Values["action"] = action;

    controller.ViewData.Model = new HandleErrorInfo(ex, currentController, currentAction);
    ((IController)controller).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
}

}

This achieves the same thing - but now you use dependency injection and you have the ability to register several ExceptionHandlers and resolve the services based on the exception type.

Share:
17,061
Arkadiusz Kałkus
Author by

Arkadiusz Kałkus

I’m a full-stack developer specialized in the design, and implementation of corporate web applications. I put emphasis on S.O.L.I.D. craftsmanship and strive to keep my code clean. Because I know how expensive technical debt can be. Because I understand my job is not to write code but to solve problems. Because I want to help people to be more effective through the software.

Updated on July 23, 2022

Comments

  • Arkadiusz Kałkus
    Arkadiusz Kałkus over 1 year

    I know it's a common issue but I've crawled many discussions with no result.

    I'm trying to handle errors with the HandleError ASP.MVC attrbiute. I'm using MVC 4.

    My Error page is places in Views/Shared/Error.cshtml and looks like that:

    Test error page
    <hgroup class="title">
        <h1 class="error">Error.</h1>
        <h2 class="error">An error occurred while processing your request.</h2>
    </hgroup>
    

    My FilterConfig.cs in the App-Start folder is:

    public class FilterConfig
        {
            public static void RegisterGlobalFilters(GlobalFilterCollection filters)
            {
                filters.Add(new HandleErrorAttribute());
            }
        }
    

    My controller:

    public class TestController : Controller
        {
            [HandleError(View = "Error")]
            public ActionResult Index()
            {
                throw new Exception("oops");
            }
        }
    

    And finally my Web.config in has the following node:

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

    When I call the controller action I get a white screen with a following text:

    Server Error in '/' Application.

    Runtime Error Description: An exception occurred while processing your request. Additionally, another exception occurred while executing the custom error page for the first exception. The request has been terminated.

    If defaultRedirect="Error" is not set in the Web.config then I get yellow screen with a following text:

    Server Error in '/' Application.

    Runtime Error Description: An application error occurred on the server. The current custom error settings for this application prevent the details of the application error from being viewed.

    Details: To enable the details of this specific error message to be viewable on the local server machine, please create a tag within a "web.config" configuration file located in the root directory of the current web application. This tag should then have its "mode" attribute set to "RemoteOnly". To enable the details to be viewable on remote machines, please set "mode" to "Off".

    Notes: The current error page you are seeing can be replaced by a custom error page by modifying the "defaultRedirect" attribute of the application's configuration tag to point to a custom error page URL.

    Does anybody know what can be wrong?

    EDIT:

    Errors were caused by using strongly typed layout. When error is thrown MVC's error handling mechanism is creating HandleErrorInfo object which is passed to the Error view. However if we use strongly typed layout then types doesn't match.

    Solution in my case is using Application_Error method in Global.asax, which was perfectly described by the SBirthare below.

  • Arkadiusz Kałkus
    Arkadiusz Kałkus over 9 years
    Thank you for reply. Now my Web.config is: <customErrors mode="On"></customErrors> And my TestController.cs is: public class TestController : Controller { public ActionResult Index() { throw new Exception("oops"); } } But I'm still getting the second described yellow screen, not my error page.
  • Arkadiusz Kałkus
    Arkadiusz Kałkus over 9 years
    Ok, I think it's something specific to my project because in default MVC project it works, so thank you, I'll tinker myself now. It's something with layout I think which is strongly typed.
  • Arkadiusz Kałkus
    Arkadiusz Kałkus over 9 years
    Thank you for your reply. Interesting approach. I'll think about it.
  • Arkadiusz Kałkus
    Arkadiusz Kałkus over 9 years
    Your solution seems to fit my needs, thank you so much!
  • David Létourneau
    David Létourneau about 8 years
    Your solution give me a ERR_TOO_MANY_REDIRECTS error :(
  • SBirthare
    SBirthare about 8 years
    @DavidLétourneau It seems you are redirecting to a page that is causing the error again. So its loop and too many redirect.
  • David Létourneau
    David Létourneau about 8 years
    @SBirthare: I found I should use Server.TransfertRequest instead of ((IController)controller).Execute. Here my answer: stackoverflow.com/questions/35385405/…
  • Simple Fellow
    Simple Fellow over 5 years
    if you don't use any attributes and don't register globally then how mvc is going to call it when exception is thrown. i find it way too complex
  • JDBennett
    JDBennett over 5 years
    The specific question asked how to use a global exception handler. This is A way. Not THE way.
  • Ristogod
    Ristogod almost 4 years
    Using IController.Execute doesn't work when the action on ErrorController returns a Task<IActionResult>. We get an error. "System.InvalidOperationException: The asynchronous action method 'Index' returns a Task, which cannot be executed synchronously."