10

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
  • 17,101
  • 19
  • 69
  • 108

5 Answers5

18

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.

SBirthare
  • 5,117
  • 4
  • 34
  • 59
  • Thank you for your reply. Interesting approach. I'll think about it. – Arkadiusz Kałkus Oct 14 '14 at 11:18
  • Your solution seems to fit my needs, thank you so much! – Arkadiusz Kałkus Oct 14 '14 at 13:24
  • Your solution give me a ERR_TOO_MANY_REDIRECTS error :( – David Létourneau Feb 13 '16 at 20:13
  • 1
    @DavidLétourneau It seems you are redirecting to a page that is causing the error again. So its loop and too many redirect. – SBirthare Feb 14 '16 at 03:24
  • @SBirthare: I found I should use Server.TransfertRequest instead of ((IController)controller).Execute. Here my answer: http://stackoverflow.com/questions/35385405/how-to-get-ride-of-customerrors-completely-in-web-config?noredirect=1#comment58475286_35385405 – David Létourneau Feb 14 '16 at 16:46
  • Using IController.Execute doesn't work when the action on ErrorController returns a Task. We get an error. "System.InvalidOperationException: The asynchronous action method 'Index' returns a Task, which cannot be executed synchronously." – Ristogod Apr 29 '20 at 14:35
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

Colin Bacon
  • 15,436
  • 7
  • 52
  • 72
  • Thank you for reply. Now my Web.config is: 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 Oct 14 '14 at 10:04
  • 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 Oct 14 '14 at 10:19
1

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.

JDBennett
  • 1,323
  • 17
  • 45
  • 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 – Simple Fellow Aug 12 '18 at 10:30
  • The specific question asked how to use a global exception handler. This is A way. Not THE way. – JDBennett Aug 12 '18 at 18:12
0

I can't figure out what HandleErrorAttribute actually does. It seems to do nothing at all.

Anyway it only takes 4 lines of code in OnException() to make it behave like I expected:

    // Copyright(c) 2016 Google Inc.
    //
    // Licensed under the Apache License, Version 2.0 (the "License"); you may not
    // use this file except in compliance with the License. You may obtain a copy of
    // the License at
    //
    // http://www.apache.org/licenses/LICENSE-2.0
    //
    // Unless required by applicable law or agreed to in writing, software
    // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    // License for the specific language governing permissions and limitations under
    // the License.

    using System.Web.Mvc;

    namespace GoogleCloudSamples
    {
        internal class CustomHandleErrorAttribute : HandleErrorAttribute
        {
            public override void OnException(ExceptionContext filterContext)
            {
                // Why oh Why doesn't base.OnException(filterContext) do this?
                ViewDataDictionary viewData = new ViewDataDictionary(filterContext);
                filterContext.Result = new ViewResult() { ViewName = "Error", ViewData = viewData };
                filterContext.HttpContext.Response.StatusCode = 500;
                filterContext.ExceptionHandled = true;
            }
        }

        public class FilterConfig
        {
            public static void RegisterGlobalFilters(GlobalFilterCollection filters)
            {
                filters.Add(new CustomHandleErrorAttribute());
            }
        }
    }
Jeffrey Rennie
  • 3,193
  • 1
  • 18
  • 19
0

SOLUTION:

First, remove the "defaultRedirect" attribute in your web.config file.

Second, in your FilterConfig.cs file, Ive found some people are referencing a "custom class" version of the "HandleErrorAttribute" found in some MVC Templates, which some developers have created online for others. These are disconnecting the ORIGINAL MVC HandlerErrorAttribute class from the default Error View Page.

You can fix this by making sure your reference the ORIGINAL Microsoft MVC HandleErrorAttribute in the FilterConfig file with the "using" statement as follows, adding a global "Error" View to it to make sure that page is now called again. See below....

using System.Web.Mvc;//add this to make sure you are referencing the MVC version
public class FilterConfig
{
    public static void Configure(System.Web.Mvc.GlobalFilterCollection filters)
    {
        // Note I added {View = "Error"}. This applies the Error View Page to all Actions in all Controller classes
        filters.Add(new HandleErrorAttribute { View = "Error" });
    }
}

This will globally assign the "Error.cshtml" view in Shared view folder to every exception thrown, except of course 404 and other server errors. Those you can handle in other ways outlined by developers above. But that should route your .NET exceptions to your custom error page. - Stokely

Stokely
  • 12,444
  • 2
  • 35
  • 23