Override `HandleErrorAttribute` in ASP.NET MVC - asp.net-mvc

I want to override the HandleErrorAttribute with a new version called something like HandleErrorsWithLogging. Essentially, I want it to log the unhandled exception to my log file and then proceed with the typical 'Redirect to ~/Shared/Error' functionality you get with HandleError.
I am already adding HandleError to all my actions in the global.asax
Is this the best approach or is there some easier way to gain access to that unhandled exception for logging purposes? I had also though about an Ajax call on the Error view itself.

You can create a custom filter that inherits from FilterAttribute and implements IExceptionFilter. Then register it in global.asax.cs. Also you must enable custom errors handling in the web.config:
<customErrors mode="On"/>
public class HandleErrorAndLogExceptionAttribute : FilterAttribute, IExceptionFilter
{
/// <summary>
/// The method called when an exception happens
/// </summary>
/// <param name="filterContext">The exception context</param>
public void OnException(ExceptionContext filterContext)
{
if (filterContext != null && filterContext.HttpContext != null)
{
if (!filterContext.IsChildAction && (!filterContext.ExceptionHandled && filterContext.HttpContext.IsCustomErrorEnabled))
{
// Log and email the exception. This is using Log4net as logging tool
Logger.LogError("There was an error", filterContext.Exception);
string controllerName = (string)filterContext.RouteData.Values["controller"];
string actionName = (string)filterContext.RouteData.Values["action"];
HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
// Set the error view to be shown
ViewResult result = new ViewResult
{
ViewName = "Error",
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
result.ViewData["Description"] = filterContext.Controller.ViewBag.Description;
filterContext.Result = result;
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
}
}

Overriding the HandleError attribute is indeed one approach into handling this. There are other approaches as well. Another approach is to use ELMAH which will take care of this so that you shouldn't worry about logging. Yet another approach consists in removing any HandleError global filters from Global.asax and subscribe for the Application_Error which is more general than HandleError as it will intercept also exceptions that happen outside of the MVC pipeline. Here's an example of such handling.

Related

How can I use data placed into a ViewBag by a filter in my Error.cshtml view?

I have an action filter that is responsible for placing some common information into the ViewBag for use by all views in the shared _Layout.cshtml file.
public class ProductInfoFilterAttribute : ActionFilterAttribute
{
public override void
OnActionExecuting(ActionExecutingContext filterContext)
{
// build product info
// ... (code omitted)
dynamic viewBag = filterContext.Controller.ViewBag;
viewBag.ProductInfo = info;
}
}
In the shared _Layout.cshtml file, I use the information that has been put into the ViewBag.
...
#ViewBag.ProductInfo.Name
...
If an exception occurs while processing a controller action, the standard HandleErrorAttribute should display my shared Error.cshtml view, and this worked before I introduced the action filter above and started using the new values from ViewBag in _Layout.cshtml. Now what I get is the standard ASP.Net runtime error page instead of my custom Error.cshtml view.
I have tracked this down to the fact that while rendering the error view, a RuntimeBinderException ("Cannot perform runtime binding on a null reference") is thrown on the use of ViewBag.ProductInfo.Name in _Layout.cshtml.
It appears that even though my action filter has successfully set the value in the ViewBag before the original exception was thrown, a new context with an empty ViewBag is used when rendering my Error.cshtml view.
Is there any way to get data created by an action filter to be available to a custom error view?
I have come up with my own solution through the addition of another filter.
public class PreserveViewDataOnExceptionFilter : IExceptionFilter
{
public void
OnException(ExceptionContext filterContext)
{
// copy view data contents from controller to result view
ViewResult viewResult = filterContext.Result as ViewResult;
if ( viewResult != null )
{
foreach ( var value in filterContext.Controller.ViewData )
{
if ( ! viewResult.ViewData.ContainsKey(value.Key) )
{
viewResult.ViewData[value.Key] = value.Value;
}
}
}
}
public static void
Register()
{
FilterProviders.Providers.Add(new FilterProvider());
}
private class FilterProvider : IFilterProvider
{
public IEnumerable<Filter>
GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
{
// attach filter as "first" for all controllers / actions; note: exception filters run in reverse order
// so this really causes the filter to be the last filter to execute
yield return new Filter(new PreserveViewDataOnExceptionFilter(), FilterScope.First, null);
}
}
}
This filter needs to be hooked in globally in the Global.asax.cs Application_Start() method by calling PreserveViewDataOnExceptionFilter.Register().
What I've done here is to set up a new exception filter that runs last, after the HandleErrorAttribute filter runs, and copies the contents of the ViewData collection that was available to the controller that threw the exception into the result created by the HandleErrorAttribute filter.

Exception Handling in ASP.NET MVC and Ajax - [HandleException] filter

All,
I'm learning MVC and using it for a business app (MVC 1.0).
I'm really struggling to get my head around exception handling. I've spent a lot of time on the web but not found anything along the lines of what I'm after.
We currently use a filter attribute that implements IExceptionFilter. We decorate a base controller class with this so all server side exceptions are nicely routed to an exception page that displays the error and performs logging.
I've started to use AJAX calls that return JSON data but when the server side implementation throws an error, the filter is fired but the page does not redirect to the Error page - it just stays on the page that called the AJAX method.
Is there any way to force the redirect on the server (e.g. a ASP.NET Server.Transfer or redirect?)
I've read that I must return a JSON object (wrapping the .NET Exception) and then redirect on the client, but then I can't guarantee the client will redirect... but then (although I'm probably doing something wrong) the server attempts to redirect but then gets an unauthorised exception (the base controller is secured but the Exception controller is not as it does not inherit from this)
Has anybody please got a simple example (.NET and jQuery code). I feel like I'm randomly trying things in the hope it will work
Exception Filter so far...
public class HandleExceptionAttribute : FilterAttribute, IExceptionFilter
{
#region IExceptionFilter Members
public void OnException(ExceptionContext filterContext)
{
if (filterContext.ExceptionHandled)
{
return;
}
filterContext.Controller.TempData[CommonLookup.ExceptionObject] = filterContext.Exception;
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = AjaxException(filterContext.Exception.Message, filterContext);
}
else
{
//Redirect to global handler
filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = AvailableControllers.Exception, action = AvailableActions.HandleException }));
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
}
}
#endregion
private JsonResult AjaxException(string message, ExceptionContext filterContext)
{
if (string.IsNullOrEmpty(message))
{
message = "Server error"; //TODO: Replace with better message
}
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true; //Needed for IIS7.0
return new JsonResult
{
Data = new { ErrorMessage = message },
ContentEncoding = Encoding.UTF8,
};
}
}
I use the OnFailure hanlder in Ajax.Beginform(). The client-side failure handler can redirect by setting window.location (among a number of other options.) This will work in 99% of modern browsers- if the browser supports AJAX it should support this.

Redirect asp.net mvc if custom exception is thrown

I need to globally redirect my users if a custom error is thrown in my application. I have tried putting some logic into my global.asax file to search for my custom error and if it's thrown, perform a redirect, but my application never hits my global.asax method. It keeps giving me an error that says my exception was unhandled by user code.
here's what I have in my global.
protected void Application_Error(object sender, EventArgs e)
{
if (HttpContext.Current != null)
{
Exception ex = HttpContext.Current.Server.GetLastError();
if (ex is MyCustomException)
{
// do stuff
}
}
}
and my exception is thrown as follows:
if(false)
throw new MyCustomException("Test from here");
when I put that into a try catch from within the file throwing the exception, my Application_Error method never gets reached. Anyone have some suggestions as to how I can handle this globally (deal with my custom exception)?
thanks.
1/15/2010 edit:
Here is what is in // do stuff.
RequestContext rc = new RequestContext(filterContext.HttpContext, filterContext.RouteData);
string url = RouteTable.Routes.GetVirtualPath(rc, new RouteValueDictionary(new { Controller = "Home", action = "Index" })).VirtualPath;
filterContext.HttpContext.Response.Redirect(url, true);
You want to create a customer filter for your controllers / actions. You'll need to inherit from FilterAttribute and IExceptionFilter.
Something like this:
public class CustomExceptionFilter : FilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext filterContext)
{
if (filterContext.Exception.GetType() == typeof(MyCustomException))
{
//Do stuff
//You'll probably want to change the
//value of 'filterContext.Result'
filterContext.ExceptionHandled = true;
}
}
}
Once you've created it, you can then apply the attribute to a BaseController that all your other controllers inherit from to make it site wide functionality.
These two articles could help:
Filters in ASP.NET MVC - Phil Haack
Understanding Action Filters
I found this answer (and question) to be helpful Asp.net mvc override OnException in base controller keeps propogating to Application_Error
Im your case the thing that you're missing is that you need to add your custom filter to the FilterConfig.cs in your App_Start folder:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new CustomExceptionFilter());
filters.Add(new HandleErrorAttribute());
}

Is there a way in ASP.NET MVC to handle different Response.StatusCode values?

By default, the MVC Authorize attribute sets the HttpContext.Response.StatusCode = 401 when a user is not authorized and the section in the web.config routes to the loginUrl property.
I want to do something similar with other response codes. For example, I have an attribute called ActiveAccount which verifies the user's account is currently active and then allows them access to the controller. If they are not active I want to route them to a specific controller and view (to update their account).
I'd like to copy the Authorize attributes way of handling this and set the StatusCode to something like 410 (warning: preceding number pulled out of thin air) and have the user routed to a location defined in the web.config file.
What can I do to implement this behavior? Or is there a simpler method?
Edit: Results
I ended up avoiding the StatusCode and just performing a redirection from within the attribute as this was much simpler. Here is my code in a nutshell:
// using the IAuthorizationFilter allows us to use the base controller's
// built attribute handling. We could have used result as well, but Auth seems
// more appropriate.
public class ActiveAccountAttribute: FilterAttribute, IAuthorizationFilter
{
#region IAuthorizationFilter Members
public void OnAuthorization(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
return;
// ... code which determines if our customer account is Active
if (!user.Status.IsActive)
filterContext.Result = new RedirectToRouteResult("Default", new RouteValueDictionary(new {controller = "Account"}));
}
#endregion
}
You could inherit the RedirectToRouteResult class and add a constructor parameter for the status code.
public class StatusRedirectResult : RedirectToRouteResult
private string _status;
public StatusRedirectResult(string action, RouteValueDictionary routeValues, string statusCode)
{
_status = statusCode;
base.RedirectToRouteResult(action, routeValues);
}
public override ExecuteResult(ControllerContext context)
{
context.HttpContext.Current.Response.Status = _status;
base.ExecuteResult(context);
}
}
To use this in a controller action, just
return StatusRedirect("NewAction", new RouteValueDictionary(new { controller = "TheController" }, "410");
it should be two piece of code.
in your controller you return an error code by
Response.StatusCode = (int)HttpStatusCode.NotFound;
then you custom error code to desired route
<customErrors mode="On">
<error statusCode="404" redirect="~/Profile/Update" />
</customErrors>

Logging errors in ASP.NET MVC

I'm currently using log4net in my ASP.NET MVC application to log exceptions. The way I'm doing this is by having all my controllers inherit from a BaseController class. In the BaseController's OnActionExecuting event, I log any exceptions that may have occurred:
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
// Log any exceptions
ILog log = LogManager.GetLogger(filterContext.Controller.GetType());
if (filterContext.Exception != null)
{
log.Error("Unhandled exception: " + filterContext.Exception.Message +
". Stack trace: " + filterContext.Exception.StackTrace,
filterContext.Exception);
}
}
This works great if an unhandled exception occurred during a controller action.
As for 404 errors, I have a custom error set up in my web.config like so:
<customErrors mode="On">
<error statusCode="404" redirect="~/page-not-found"/>
</customErrors>
And in the controller action that handles the "page-not-found" url, I log the original url being requested:
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult PageNotFound()
{
log.Warn("404 page not found - " + Utils.SafeString(Request.QueryString["aspxerrorpath"]));
return View();
}
And this also works.
The problem that I'm having is how to log errors that are on the .aspx pages themselves. Let's say I have a compilation error on one of the pages or some inline code that will throw an exception:
<% ThisIsNotAValidFunction(); %>
<% throw new Exception("help!"); %>
It appears that the HandleError attribute is correctly rerouting this to my Error.aspx page in the Shared folder, but it is definitely not being caught by my BaseController's OnActionExecuted method. I was thinking I could maybe put the logging code on the Error.aspx page itself, but I'm unsure of how to retrieve the error information at that level.
I would consider simplifying your web application by plugging in Elmah.
You add the Elmah assembly to your project and then configure your web.config. It will then log exceptions created at controller or page level. It can be configured to log to various different places (like SQL Server, Email etc). It also provides a web frontend, so that you can browse through the log of exceptions.
Its the first thing I add to any asp.net mvc app I create.
I still use log4net, but I tend to use it for logging debug/info, and leave all exceptions to Elmah.
You can also find more information in the question How do you log errors (Exceptions) in your ASP.NET apps?.
You can hook into the OnError event in the Global.asax.
Something like this:
/// <summary>
/// Handles the Error event of the Application control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
protected void Application_Error(object sender, EventArgs e)
{
if (Server != null)
{
Exception ex = Server.GetLastError();
if (Response.StatusCode != 404 )
{
Logging.Error("Caught in Global.asax", ex);
}
}
}
MVC3
Create Attribute that inherits from HandleErrorInfoAttribute and includes your choice of logging
public class ErrorLoggerAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
LogError(filterContext);
base.OnException(filterContext);
}
public void LogError(ExceptionContext filterContext)
{
// You could use any logging approach here
StringBuilder builder = new StringBuilder();
builder
.AppendLine("----------")
.AppendLine(DateTime.Now.ToString())
.AppendFormat("Source:\t{0}", filterContext.Exception.Source)
.AppendLine()
.AppendFormat("Target:\t{0}", filterContext.Exception.TargetSite)
.AppendLine()
.AppendFormat("Type:\t{0}", filterContext.Exception.GetType().Name)
.AppendLine()
.AppendFormat("Message:\t{0}", filterContext.Exception.Message)
.AppendLine()
.AppendFormat("Stack:\t{0}", filterContext.Exception.StackTrace)
.AppendLine();
string filePath = filterContext.HttpContext.Server.MapPath("~/App_Data/Error.log");
using(StreamWriter writer = File.AppendText(filePath))
{
writer.Write(builder.ToString());
writer.Flush();
}
}
Place attribute in Global.asax RegisterGlobalFilters
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
// filters.Add(new HandleErrorAttribute());
filters.Add(new ErrorLoggerAttribute());
}
Have you thought about extending the HandleError attribute? Also, Scott has a good blog post about filter interceptors on controllers/ actions here.
The Error.aspx view is defined like this:
namespace MvcApplication1.Views.Shared
{
public partial class Error : ViewPage<HandleErrorInfo>
{
}
}
The HandleErrorInfo has three properties:
string ActionName
string ControllerName
Exception Exception
You should be able to access HandleErrorInfo and therefore the Exception within the view.
You can try to examine HttpContext.Error, but I am not sure on this.

Resources