Does the filterContext.ExceptionHandled property ever get set to true by MVC or is it only set to true by user code? If so where does this happen?
filterContext.ExceptionHandled get set to true when an exception is thrown by an action method. By default HandleErrorAttribute has been added in FilterConfig class which is registered in Application_Start(). When an exception occurs, the OnException method is called in HandleErrorAttribute class.
In OnException method, before removing the current HTTP response body by using Response.Clear(), the ExceptionHandled property will set to true.
Below is the default OnException method:
public virtual void OnException(ExceptionContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (filterContext.IsChildAction)
{
return;
}
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
{
return;
}
Exception exception = filterContext.Exception;
if (new HttpException(null, exception).GetHttpCode() != 500)
{
return;
}
if (!ExceptionType.IsInstanceOfType(exception))
{
return;
}
string controllerName = (string)filterContext.RouteData.Values["controller"];
string actionName = (string)filterContext.RouteData.Values["action"];
HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
filterContext.Result = new ViewResult
{
ViewName = View,
MasterName = Master,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
Related
I am using the filter from the solution posted to Clean way to catch all errors thrown in an MVC 3.0 application?, and I was wondering how I could redirect to a view (of a simple error page that resides in /Views/Account/ErrorPage, as part of the AccountController), in that filter? It is reproduced below:
public class HandleExceptionsAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
var expception = filterContext.Exception;
// Go to generic error page:
}
}
Thank you.
Set the filterContext.Result
filterContext.Result = new ViewResult
{
ViewName = "<view name>",
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
Here's the full method that captures the call that errors
public override void OnException(ExceptionContext filterContext)
{
base.OnException(filterContext);
var controllerName = (string) filterContext.RouteData.Values["controller"];
var actionName = (string) filterContext.RouteData.Values["action"];
var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
filterContext.Result = new ViewResult
{
ViewName = "<view name>",
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
How best to create a base controller in a new ASP.NET MVC application that will contain boilerplate code to handle all try/catch routines for every action in derived controllers and return a standard JSON error message after logging to Nlog. I would also like to handle 404 errors to redirect to a custom 404 view. I aim using OWIN cookie authentication and would like to signout when the cookie expires. All my actions will be returning JsonResult and invoked via jQuery Ajax.
In previous projects I've used the following approach:
In Global.asax.cs
protected void Application_Error(object sender, EventArgs e)
{
Exception lastError = Server.GetLastError();
Server.ClearError();
var statusCode = 0;
statusCode = lastError.GetType() == typeof(HttpException) ? ((HttpException)lastError).GetHttpCode() : 500;
var routeData = new RouteData();
routeData.Values.Add("controller", "Error");
routeData.Values.Add("statusCode", statusCode);
routeData.Values.Add("exception", lastError);
if (new HttpRequestWrapper(HttpContext.Current.Request).IsAjaxRequest())
{
routeData.Values.Add("action", "Ajax");
}
else
{
routeData.Values.Add("action", "Index");
}
var requestContext = new RequestContext(new HttpContextWrapper(Context), routeData);
IController controller = new ErrorController();
controller.Execute(requestContext);
Response.End();
}
In ErrorController.cs
public class ErrorController : Controller
{
public ActionResult Index(int statusCode, Exception exception)
{
var model = new ErrorModel { HttpStatusCode = statusCode, Exception = exception.Message };
Response.StatusCode = statusCode;
return View(model);
}
public JsonResult Ajax(int statusCode, Exception exception, Dictionary<string,string> validationErrors = null)
{
var model = new ErrorModel { HttpStatusCode = statusCode, Exception = exception.Message };
if (exception.GetType() == typeof (DbEntityValidationException))
{
model.ValidationErrors = null;
}
else
{
model.ValidationErrors = validationErrors;
}
Response.StatusCode = statusCode;
return Json(model, JsonRequestBehavior.AllowGet);
}
}
The best way of doing that is by extending the HandleErrorAttribute like this:
public class AjaxAwareHandleErrorAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
// Execute the normal exception handling routine...
base.OnException(filterContext);
// Verify if AJAX request...
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
// Use Json in case of AJAX request...
var result = new JsonResult();
result.Data = new { Error = filterContext.Exception.Message };
filterContext.Result = result;
}
}
}
Then, all you need to do is to register that attribute in your FilterConfig class under the App_Start folder like this:
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new AjaxAwareHandleErrorAttribute());
}
}
I search for a generic way to display thrown exceptions without redirecting to an error page but displaying it in the same view. I tried these below:
1) I firstly tried to handle them by adding a custom filter in global.asax and overriding public override void OnException(ExceptionContext filterContext) in my Attribute class but in that way, I couldn't fill filterContext.Result in the way I want since the old model of the view is not reachable so I could only redirect to an error page but that's not what I want.
2) Then I tried to catch the exceptions on my BaseController(All of my controllers inherits from it). I again override public override void OnException(ExceptionContext filterContext) in my controller and put exception details etc. in ViewBag and redirected the page to the same view by filterContext.HttpContext.Response.Redirect(filterContext.RequestContext.HttpContext.Request.Path ); but ViewBag contents are lost in the redirected page so I can't think any other way?
How can I achieve that? Code Sample that I wrote in my BaseController is below:
protected override void OnException(ExceptionContext filterContext) {
var controllerName = (string)filterContext.RouteData.Values["controller"];
var actionName = (string)filterContext.RouteData.Values["action"];
//filterContext.Result = new ViewResult
//{
// ViewName = actionName,
// ViewData = new ViewDataDictionary<??>(??),
// TempData = filterContext.Controller.TempData,
//};
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
ModelState.AddModelError("Error", filterContext.Exception.Message);
ViewBag.das = "dasd";
filterContext.HttpContext.Response.Redirect(filterContext.RequestContext.HttpContext.Request.Path);
}
Maybe you could set a property in your BaseController class to have the name of the view that you want to use, setting that in whatever controller action handles the request. Then in OnException() you could have a method, that redirects to a controller action, that just returns a View that corresponds to the view name? Each controller action would have to set a default view name before it does anything else because only it knows what view it will call if any, and what view it likely was invoked by.
You'd need some sort of BaseController action that returns the new View.
The route(s) may or many not need configuration to have some sort of optional parameter(s) that you could set to be what error information you want to send to your view. For example, in the default route:
routes.MapRoute(RouteNames.Default,
"{controller}/{action}/{id}",
new {controller = "Home", action = "Index", id = "", errorInfo = UrlParameter.Optional}
BaseController:
protected ActionResult ErrorHandler()
{
ViewBag.das = (string)filterContext.RouteData.Values["errorInfo"];
return View(ViewName);
}
protected string ViewName { get; set; }
protected void GoToErrorView(ExceptionContext context, string exceptionData)
{
var actionName = "ErrorHandler";
var newVals = new RouteValueDictionary();
newVals.Add("errorInfo", exceptionData);
this.RedirectToAction(actionName, newVals);
}
In BaseController.OnException():
// ...
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
ModelState.AddModelError("Error", filterContext.Exception.Message);
// anything else you need to do to prepare what you want to display
string exceptionData = SomeSortOfDataYouWantToPassIntoTheView;
this.GoToErrorView(filterContext, exceptionData);
}
In the specific controllers that inherit from BaseController that are returning an ActionResult specifically a ViewResult:
[HttpGet]
public ActionResult Index()
{
ViewName = <set whatever view name you want to here>
// code here, including preparing the Model
// ...
var model = new MyViewModel();
model.SomethingIWantToGiveTheView = someDataThatItNeeds;
// ...
return View(<model name>, model);
}
I found the solution a while ago and add the solution so that it may help the others. I use TempData and _Layout to display errors:
public class ErrorHandlerAttribute : HandleErrorAttribute
{
private ILog _logger;
public ErrorHandlerAttribute()
{
_logger = Log4NetManager.GetLogger("MyLogger");
}
public override void OnException(ExceptionContext filterContext)
{
if (filterContext.ExceptionHandled)
{
return;
}
if (!ExceptionType.IsInstanceOfType(filterContext.Exception))
{
return;
}
// if the request is AJAX return JSON else view.
if (filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
filterContext.Result = new JsonResult
{
JsonRequestBehavior = JsonRequestBehavior.AllowGet,
Data = new
{
error = true,
message = filterContext.Exception.Message
}
};
filterContext.HttpContext.Response.StatusCode = 500;
}
// log the error using log4net.
_logger.Error(filterContext.Exception.Message, filterContext.Exception);
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
if (filterContext.HttpContext.Request.Headers["X-Requested-With"] != "XMLHttpRequest")
{
if (filterContext.Controller.TempData["AppError"] != null)
{
//If there is a loop it will break here.
filterContext.Controller.TempData["AppError"] = filterContext.Exception.Message;
filterContext.HttpContext.Response.Redirect("/");
}
else
{
int httpCode = new HttpException(null, filterContext.Exception).GetHttpCode();
switch (httpCode)
{
case 401:
filterContext.Controller.TempData["AppError"] = "Not Authorized";
filterContext.HttpContext.Response.Redirect("/");
break;
case 404:
filterContext.Controller.TempData["AppError"] = "Not Found";
filterContext.HttpContext.Response.Redirect("/");
break;
default:
filterContext.Controller.TempData["AppError"] = filterContext.Exception.Message;
//Redirect to the same page again(If error occurs again, it will break above)
filterContext.HttpContext.Response.Redirect(filterContext.RequestContext.HttpContext.Request.RawUrl);
break;
}
}
}
}
}
And in Global.asax:
protected void Application_Error(object sender, EventArgs e)
{
var httpContext = ((MvcApplication)sender).Context;
var ex = Server.GetLastError();
httpContext.ClearError();
httpContext.Response.Clear();
httpContext.Response.StatusCode = ex is HttpException ? ((HttpException)ex).GetHttpCode() : 500;
httpContext.Response.TrySkipIisCustomErrors = true;
var routeData = new RouteData();
routeData.Values["controller"] = "ControllerName";
routeData.Values["action"] = "ActionName";
routeData.Values["error"] = "404"; //Handle this url paramater in your action
((IController)new AccountController()).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
}
I have started an MVC 3 template project in VS10 and modified global.asax.cs as such:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute { ExceptionType = typeof(DivideByZeroException), View = "DivideByZeroException", Order = 1 });
filters.Add(new HandleErrorAttribute { View = "AllOtherExceptions", Order = 2 });
}
To web.config I added:
<customErrors mode="On">
Then created the corresponding views and finally added a DivideByZero-throw to one of the actions.
The result: The view AllOtherExceptions is rendered.
Much though I hate to disagree with anything Darin says, he is wrong on this one.
There is no problem with setting the properties (that's the way you are supposed to do it).
The only reason your original code didn't work as expected is because you have the Order set wrong.
See MSDN:
The OnActionExecuting(ActionExecutingContext),
OnResultExecuting(ResultExecutingContext), and
OnAuthorization(AuthorizationContext) filters run in forward order.
The OnActionExecuted(ActionExecutedContext),
OnResultExecuting(ResultExecutingContext), and
OnException(ExceptionContext) filters run in reverse order.
So your generic AllOtherExceptions filter needs to be the lowest Order number, not the highest.
Hopefully that helps for next time.
You shouldn't set properties when registering a global action filter. You could write a custom handle error filter:
public class MyHandleErrorAttribute : FilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext filterContext)
{
if (!filterContext.IsChildAction && (!filterContext.ExceptionHandled && filterContext.HttpContext.IsCustomErrorEnabled))
{
Exception innerException = filterContext.Exception;
if ((new HttpException(null, innerException).GetHttpCode() == 500))
{
var viewName = "AllOtherExceptions";
if (typeof(DivideByZeroException).IsInstanceOfType(innerException))
{
viewName = "DivideByZeroException";
}
string controllerName = (string)filterContext.RouteData.Values["controller"];
string actionName = (string)filterContext.RouteData.Values["action"];
HandleErrorInfo model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
ViewResult result = new ViewResult
{
ViewName = viewName,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
filterContext.Result = result;
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
}
}
}
and then register it:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new MyHandleErrorAttribute());
}
Do check fear's answer below. It's certainly the simpler, if it works.
Since it came after a few weeks, this is how my filter finally spelled out, using Darins response and incorporating Elmah-reporting, with code from this topic.
I still don't know why you can't set properties on a global action filter.
public class MyHandleErrorAttribute : FilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext filterContext)
{
if (!filterContext.IsChildAction &&
(!filterContext.ExceptionHandled && filterContext.HttpContext.IsCustomErrorEnabled))
{
var innerException = filterContext.Exception;
if ((new HttpException(null, innerException).GetHttpCode() == 500))
{
var viewName = "GeneralError";
if (typeof (HttpAntiForgeryException).IsInstanceOfType(innerException))
viewName = "SecurityError";
var controllerName = (string) filterContext.RouteData.Values["controller"];
var actionName = (string) filterContext.RouteData.Values["action"];
var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
var result = new ViewResult
{
ViewName = viewName,
ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
TempData = filterContext.Controller.TempData
};
filterContext.Result = result;
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
//From here on down, this is all code for Elmah-reporting.
var version = Assembly.GetExecutingAssembly().GetName().Version;
filterContext.Controller.ViewData["Version"] = version.ToString();
var e = filterContext.Exception;
if (!filterContext.ExceptionHandled // if unhandled, will be logged anyhow
|| RaiseErrorSignal(e) // prefer signaling, if possible
|| IsFiltered(filterContext)) // filtered?
return;
LogException(e);
}
}
}
private static bool RaiseErrorSignal(Exception e)
{
HttpContext context = HttpContext.Current;
if (context == null)
return false;
var signal = ErrorSignal.FromContext(context);
if (signal == null)
return false;
signal.Raise(e, context);
return true;
}
private static bool IsFiltered(ExceptionContext context)
{
var config = context.HttpContext.GetSection("elmah/errorFilter")
as ErrorFilterConfiguration;
if (config == null)
return false;
var testContext = new ErrorFilterModule.AssertionHelperContext(
context.Exception, HttpContext.Current);
return config.Assertion.Test(testContext);
}
private static void LogException(Exception e)
{
HttpContext context = HttpContext.Current;
ErrorLog.GetDefault(context).Log(new Error(e, context));
}
}
I wrote my own HandleError attribute.
When an error occurs during an ajax request I want to return a partialview and when the request is non ajax a view with master page should be returned.
So far I wrote this
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class IcpHandleErrorAttribute : FilterAttribute, IExceptionFilter
{
private readonly Type _exceptionType = typeof(Exception);
public IcpHandleErrorAttribute()
{}
public void OnException(ExceptionContext filterContext)
{
if (filterContext == null)throw new ArgumentNullException("filterContext");
if (filterContext.IsChildAction)return;
if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)return;
Exception exception = filterContext.Exception;
if (new HttpException(null, exception).GetHttpCode() != 500)return;
if (!_exceptionType.IsInstanceOfType(exception))return;
var controllerName = (string)filterContext.RouteData.Values["controller"];
var actionName = (string)filterContext.RouteData.Values["action"];
var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
ViewResultBase result;
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
result = new PartialViewResult { ViewName = "ErrorAjax" };
}
else
{
result = new ViewResult{ViewName = "Error"};
}
result.ViewData = new ViewDataDictionary<HandleErrorInfo>(model);
result.TempData = filterContext.Controller.TempData;
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = 500;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
}
But the masterpage is returned even when the PartialViewResult is assigned to the result.
Why is this happening ?
The ErrorAjax partial view
#model HandleErrorInfo
#{
Layout = null;
}
<div class="error">#Model.Exception.Message</div>
The action is called through jquery's post.
The action method is a test case
[HttpPost]
public ActionResult Create(ProjectCreateCommand command)
{
throw new NotImplementedException("ajax");
return Post(command);
}
The attribte registration in global.asax 's Application_Start() method.
GlobalFilters.Filters.Add(new IcpHandleErrorAttribute());
RegisterGlobalFilters(GlobalFilters.Filters);
You don't seem to be doing anything useful with your result local variable like for example assigning it to the filter context:
filterContext.Result = result;
If this not Ajax request you need redirect to some Action. And in this action return View()