views:

4948

answers:

6

How can I correctly handle exceptions thrown from controllers in ASP.NET MVC? The HandleError attribute seems to only process exceptions thrown by the MVC infrastructure and not exceptions thrown by my own code.

Using this web.config

<customErrors mode="On">
    <error statusCode="401" redirect="/Errors/Http401" />
</customErrors>

with the following code

namespace MvcApplication1.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            // Force a 401 exception for testing
            throw new HttpException(401, "Unauthorized");
        }
    }
}

doesn't result in what I was hoping for. Instead I get the generic ASP.NET error page telling me to modify my web.config to see the actual error information. However, if instead of throwing an exception I return an invalid View, I get the /Shared/Views/Error.aspx page:

return View("DoesNotExist");

Throwing exceptions within a controller like I've done above seems to bypass all of the HandleError functionality, so what's the right way to create error pages and how do I play nice with the MVC infrastructure?

+1  A: 

The HandleError attribute seems to only process exceptions thrown by the MVC infrastructure and not exceptions thrown by my own code.

That is just wrong. Indeed, HandleError will only "process" exceptions either thrown in your own code or in code called by your own code. In other words, only exceptions where your action is in the call stack.

The real explanation for the behavior you're seeing is the specific exception you're throwing. HandleError behaves differently with an HttpException. From the source code:

        // If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method),
        // ignore it.
        if (new HttpException(null, exception).GetHttpCode() != 500) {
            return;
        }
Craig Stuntz
You've pointed out that one of my statements is incorrect without addressing the real issue of not being able to provide error handling for HTTP error codes.
Adrian Anttila
I can explain what is happening, but I would need more information in order to tell you the right thing to do for your application. HandleError works on an action, and you don't generally throw HttpExceptions within an action. Rather than guess what you are actually trying to do and give you potentially incorrect advice, I can just explain the facts behind the behavior you're seeing. Please feel free to update the question if you'd like to explain more about what you really want to do.
Craig Stuntz
+13  A: 

Controller.OnException(ExceptionContext context). Override it.

 protected override void OnException(ExceptionContext filterContext)
 {
  // Bail if we can't do anything; app will crash.
  if (filterContext == null)
   return;

  // since we're handling this, log to elmah
  var ex = filterContext.Exception ?? new Exception("No further information exists.");
  LogException(ex);

  filterContext.ExceptionHandled = true;
  var data = new ErrorPresentation
   {
    ErrorMessage = HttpUtility.HtmlEncode(ex.Message),
    TheException = ex,
    ShowMessage = !(filterContext.Exception == null),
    ShowLink = false
   };
  filterContext.Result = View("ErrorPage", data);
 }
Will
Yep, this is the way to do it. Using one of the provided filters, or creating your own exception filter is the way to go. You can change the view based on the type of exception or dive deeper by inspecting more info in your custom filter.
AndrewDotHay
Are you sure this wasn't something that was in preview releases of MVC but was changed for 1.0? I don't see this override, instead it looks like you should override OnActionExecuted and inspect the filterContext.Exception value
Richard Ev
http://msdn.microsoft.com/en-us/library/system.web.mvc.controller.onexception(VS.100).aspx
Will
+3  A: 

I dont think you will be able to show specific ErrorPage based upon the HttpCode with the HandleError Attribute and I would prefer to use an HttpModule for this purpose. Assuming that I have folder "ErrorPages" where diffferent page exists for each specific error and the mapping is specifed in the web.config same as the regular web form application. And the following is the code which is used to show the error page:

public class ErrorHandler : BaseHttpModule{

public override void OnError(HttpContextBase context)
{
    Exception e = context.Server.GetLastError().GetBaseException();
    HttpException httpException = e as HttpException;
    int statusCode = (int) HttpStatusCode.InternalServerError;

    // Skip Page Not Found and Service not unavailable from logging
    if (httpException != null)
    {
        statusCode = httpException.GetHttpCode();

        if ((statusCode != (int) HttpStatusCode.NotFound) && (statusCode != (int) HttpStatusCode.ServiceUnavailable))
        {
            Log.Exception(e);
        }
    }

    string redirectUrl = null;

    if (context.IsCustomErrorEnabled)
    {
        CustomErrorsSection section = IoC.Resolve<IConfigurationManager>().GetSection<CustomErrorsSection>("system.web/customErrors");

        if (section != null)
        {
            redirectUrl = section.DefaultRedirect;

            if (httpException != null)
            {
                if (section.Errors.Count > 0)
                {
                    CustomError item = section.Errors[statusCode.ToString(Constants.CurrentCulture)];

                    if (item != null)
                    {
                        redirectUrl = item.Redirect;
                    }
                }
            }
        }
    }

    context.Response.Clear();
    context.Response.StatusCode = statusCode;
    context.Response.TrySkipIisCustomErrors = true;

    context.ClearError();

    if (!string.IsNullOrEmpty(redirectUrl))
    {
        context.Server.Transfer(redirectUrl);
    }
}

}

kazimanzurrashid
This is what I was looking for. I took a slightly simpler approach and put my code in Application_Error to handle the same set of errors across controllers. I'll post the solution in an answer to this question.
Adrian Anttila
+1  A: 

Thanks to kazimanzurrashaid, here is what I wound up doing in Global.asax.cs:

protected void Application_Error()
{
    Exception unhandledException = Server.GetLastError();
    HttpException httpException = unhandledException as HttpException;
    if (httpException == null)
    {
        Exception innerException = unhandledException.InnerException;
        httpException = innerException as HttpException;
    }

    if (httpException != null)
    {
        int httpCode = httpException.GetHttpCode();
        switch (httpCode)
        {
            case (int) HttpStatusCode.Unauthorized:
                Response.Redirect("/Http/Error401");
                break;
        }
    }
}

I'll be able to add more pages to the HttpContoller based on any additional HTTP error codes I need to support.

Adrian Anttila
I would suggest you to check the source of http://kigg.codeplex.com where you will find the complete source code of this httpModule, I just want to clutter global.asax that is why the HttpModule.
kazimanzurrashid
+1  A: 

One other possibility (not true in your case) that others reading this may be experiencing is that your error page is throwing an error itself, or is not implementing :

 System.Web.Mvc.ViewPage<System.Web.Mvc.HandleErrorInfo>

If this is the case then you will get the default error page (otherwise you'd get an infinite loop because it would keep trying to send itself to your custom error page). This wasn't immediately obvious to me.

This model is the model sent to the error page. If your error page uses the same master page as the rest of your site and requires any other model information then you will need to either create your own [HandleError] type of attribute or override OnException or something.

Simon_Weaver
You could also declare the [HandleError] attribute on your controllers or controller methods, but it doesn't help for HTTP errors.
Adrian Anttila
+1  A: 

I chose the Controller.OnException() approach, which to me is the logical choice - since I've chosen ASP.NET MVC, I prefer to stay at the framework-level, and avoid messing with the underlying mechanics, if possible.

I ran into the following problem:

If the exception occurs within the view, the partial output from that view will appear on screen, together with the error-message.

I fixed this by clearing the response, before setting filterContext.Result - like this:

        filterContext.HttpContext.Response.Clear(); // gets rid of any garbage
        filterContext.Result = View("ErrorPage", data);

Hope this saves somebody else some time :-)

mindplay.dk
It seems like you are replicating the behaviour already provided for by the HandleError attribute.
Martin Aatmaa
Possibly. Learning all of this stuff is turning out to be a painful effort. So many bad examples and outdated documentation to filter through. I guess I just contributed another bad example ;-)
mindplay.dk