A configurable error handling filter for ASP.NET MVC

The HandlerError attribute that ships with ASP.NET MVC is an exception filter that can be used to specify pairs {exception type, view name} which are used to display error pages when exceptions occur on the web application. That filter can be used at action, controller or global levels. However, it will always assume the 500 HTTP status code and, when used globally, you have to be careful with the filter registration order, because exception types are not strictly matched (it uses IsAssignableFrom)

I came across a scenario where it was useful to map certain exceptions to specific HTTP status codes. Also, it would be nice to log that exception accordingly to its severity. So I wrote a global exception filter that maps exception types to status codes and log levels (I’m using log4net on this one). It can be configured like this (with better exception types, I hope):

filters.Add(new ExceptionHandlerFilter("~/Views/Shared/Errors", LogManager.GetLogger(typeof(ExceptionHandlerFilter)))
	.Handle<NotImplementedException>(HttpStatusCode.NotFound, "NotFound")
	.Handle<NotSupportedException>(HttpStatusCode.NotImplemented, "NotFound", Level.Info));

The views are passed a model of type HandleErrorInfo, which is also used by HandleErrorAttribute.

The source code is based on the code from HandleErrorAttribute and is pretty straight forward. The filter is only active if custom erros are enabled on the current request and has fallback through exceptions hierarchy.

public class ExceptionHandlerFilter : IExceptionFilter
{
	class ExceptionHandlingInfo
	{
		public HttpStatusCode StatusCode { get; set; }
		public string View { get; set; }
		public Level LogLevel { get; set; }
	}

	private readonly string _viewsPathPrefix;
	private readonly Dictionary<Level, Action> _loggersByLevel;
	private readonly Dictionary<Type, ExceptionHandlingInfo> _exceptionHandlingInfos;

	public ExceptionHandlerFilter(string viewsPathPrefix, ILog log)
	{
		_viewsPathPrefix = viewsPathPrefix;

		_loggersByLevel = new Dictionary<Level, Action>
		{
			{ Level.Info, log.Info },
			{ Level.Warn, log.Warn },
			{ Level.Error, log.Error },
			{ Level.Fatal, log.Fatal },
		};

		_exceptionHandlingInfos = new Dictionary<Type, ExceptionHandlingInfo>();
	}

	public ExceptionHandlerFilter Handle<T>(HttpStatusCode statusCode, string view, Level logLevel = null) where T : Exception
	{
		_exceptionHandlingInfos.Add(typeof(T), new ExceptionHandlingInfo { StatusCode = statusCode, View = view, LogLevel = logLevel });
		return this;
	}

	private ExceptionHandlingInfo GetExceptionHandlingInfo(Exception ex)
	{
		Type exType = ex.GetType();
		while (exType != typeof(Exception))
		{
			ExceptionHandlingInfo info;
			if (_exceptionHandlingInfos.TryGetValue(exType, out info))
			{
				return info;
			}

			exType = exType.BaseType;
		}
		return null;
	}

	public void OnException(ExceptionContext filterContext)
	{
		if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled || filterContext.IsChildAction)
		{
			return;
		}

		var exInfo = GetExceptionHandlingInfo(filterContext.Exception);
		if (exInfo == null)
		{
				return;
		}

		if (exInfo.LogLevel != null)
		{
			_loggersByLevel[exInfo.LogLevel](filterContext.Exception);
		}

		var model = new HandleErrorInfo(filterContext.Exception, (string)filterContext.RouteData.Values["controller"], (string)filterContext.RouteData.Values["action"]);

		filterContext.Result = new ViewResult
		{
			ViewName = String.Format("{0}/{1}.cshtml", _viewsPathPrefix, exInfo.View),
			ViewData = new ViewDataDictionary(model),
			TempData = filterContext.Controller.TempData,
		};

		filterContext.ExceptionHandled = true;
		filterContext.HttpContext.Response.Clear();
		filterContext.HttpContext.Response.StatusCode = (int)exInfo.StatusCode;
		filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
	}
}

Hope this helps!

Advertisements