ASP Errors (handling exceptions)
NB: not data validation! For WebAPI it's a little different- see below
Raising errors
You can return "RESTful" HTTP status codes:
//404
if (category > 0)
return HttpNotFound("No such category"); //new HttpNotFoundResult()
//400
if (category == -1)
return new HttpStatusCodeResult(System.Net.HttpStatusCode.BadRequest,
"Logical error.");
//401, when AuthorizeAttribute is not enough
if (User.Identity.IsAuthenticated && !User.IsInRole("Admin"))
return new HttpUnauthorizedResult("Unauthorized");
//500
if (category == -2)
throw new HttpException("Generic error");
if (category == -3)
throw new InvalidOperationException("Generic error"); //also gets wrapped in HttpException
Catching errors
The stack (from closest to exception upwards)
- Controller.OnException. Rarely used.
- [HandleError]. Normally needs logging action filter as well, and just having the Shared/Error.cshtml is limiting.
- global.asax Application_Error. Good idea, if you don't do HandleError with logging filter.
- web.config customErrors. Required.
- web.config httpErrors (IIS). Normally required.
HandleError
By default, App_Start/FilterConfig will add a global [HandleError] attribute.
You can also add other HandleError attributes on controllers or actions, for specific errors and using specific views (set Order to control precedence - high order = high priority).
[HandleError(ExceptionType = typeof(InvalidOperationException))]
HandleError detects normal exceptions (not 404s) and redirects to a view Error.cshtml, either in the same controller or /Shared.
NB: system.web/customErrors[mode="On"] must be set, otherwise it ignores exceptions (and shows the YSOD, which in debug is what you want)
The default templated Error.cshtml is full html, but you can use _Layout and use a HandleErrorInfo model.
@model System.Web.Mvc.HandleErrorInfo
@{
ViewBag.Title = "Test";
}
<h1>Error Handled</h1>
<p>Controller=@Model.ControllerName action=@Model.ActionName exception=@Model.Exception.Message</p>
A view with no controller is awkward. You can't log errors (you shouldn't do logic in the view). You may prefer to not use a global HandleError and use web.config customErrors or global.asax Application_Error instead.
You can add a new logging filter in App_Start/FilterConfig before the HandleError
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new ErrorLoggerFilter());
filters.Add(new HandleErrorAttribute());
}
The Error logging filter looks like this. Substitute the logging framework of your choice.
public class ErrorLoggerFilter : IExceptionFilter
{
public void OnException(ExceptionContext filterContext)
{
System.Diagnostics.Trace.TraceError(
"LogFilter: " + filterContext.Exception);
//if using ELMAH
if (filterContext.ExceptionHandled)
Elmah.ErrorSignal.FromCurrentContext().Raise(filterContext.Exception);
}
}
Routing 404s
"Catch-all" routes (with asterisk prefix) don't work well. This has to be after the default "controller/action/id" route, but any url of the form x/y or /y/z never hits the catch all (you will trap 1/2/3/4)
routes.MapRoute(
"404",
"{*url}",
new { controller = "Error", action = "NotFound" }
);
As this is basically useless, it's better to just use customErrors, below
web.config customErrors
The traditional ASP config: system.web/customErrors[mode="On"]. By default this does a 302.
<system.web>
<customErrors mode="On" defaultRedirect="/Error/">
<error statusCode="404" redirect="/Error/NotFound" />
</customErrors>
redirectMode="ResponseRewrite" cannot redirect to a route, only to a physical html or aspx file.
[HandleError] intercepts most errors before this (but not 404s). Unlike [HandleError] you redirect to a controller, but the exception isn't passed on so you can't log it (use a logging exception filter or global.asax Application_Error).
httpErrors
This is only used by IIS7. customErrors will trap errors first, and httpErrors catches higher level IIS level exceptions like 404s on .html files.
<system.webServer>
<httpErrors errorMode="Custom">
<!-- catch all non-ASP 404s (.jpg, .html) -->
<remove statusCode="404" />
<error statusCode="404" path="/Error/NotFound" />
</httpErrors>
Error controller
You simply set the HTTP status code and Response.TrySkipIisCustomErrors to true. But you don't have access to the original exception (you arrive at the controller from a 302 redirect).
public class ErrorController : Controller
{
// GET: /Error/
public ActionResult Index()
{
Response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError; //500
Response.TrySkipIisCustomErrors = true;
return View();
}
// GET: /Error/NotFound
public ActionResult NotFound()
{
Response.StatusCode = (int)System.Net.HttpStatusCode.NotFound; //404
Response.TrySkipIisCustomErrors = true;
return View();
}
}
Global.asax Application_Error
The "classic" asp.net solution is still preferable to HandleError. It fires before customErrors. If you don't call Server.ClearError the customErrors takes over. It is not fired if there is a [HandleError] attribute. If you are not logging exceptions through an action filter, you can reliably log here.
protected void Application_Error()
{
var exception = Server.GetLastError();
Response.Clear();
//customErrors=On/RemoteOnly+nonLocal
if (!HttpContext.Current.IsCustomErrorEnabled)
return; //wants to see YSOD
//log it if necessary
System.Diagnostics.Trace.TraceError(exception.ToString());
//is this a specific error?
var httpException = exception as HttpException;
string action = null;
if (httpException != null)
{
var code = httpException.GetHttpCode();
if (code == 404)
action = "NotFound";
}
Server.ClearError(); //make sure customErrors doesn't take over
Response.TrySkipIisCustomErrors = true; //don't let IIS7 take over
Response.Redirect(String.Format("~/Error/{0}", action));
}
WebAPI
global.asax Application_Error is NOT fired, but it does obey customErrors mode for whether an exception is returned in Json/xml.
From 2014, webapi has ErrorLogger and ExceptionHandler base classes which can be inherited and applied for true global logging and handling.
if (id == 1)
throw new HttpResponseException(HttpStatusCode.NotFound);
if (id == 2)
{
var responce = new HttpResponseMessage(HttpStatusCode.NotFound);
responce.ReasonPhrase = "Item not found"; //HTTP level
responce.Content = new StringContent("No item with id 2"); //message
throw new HttpResponseException(responce);
}
HttpResponseMessage methods
if (id == 0) //createResponse
return Request.CreateResponse(HttpStatusCode.NotFound, "Cannot find id");
if (id == 1) //using HttpError
return Request.CreateResponse(HttpStatusCode.BadRequest, new HttpError("Bad input"));
if (id == 2) //createErrorResponse
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Bad input");
if (!ModelState.IsValid) //modelstate
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
The standard json error response is { Message: x }
WebAPI filters
You can use a limited equivalent by implementing the abstract ExceptionFilterAttribute. It is limited as it only traps raw .net exceptions, and ignores HttpResponseExceptions (which prior to WebApi 2.1 can't be logged...)
public class HandleExceptionAttribute : System.Web.Http.Filters.ExceptionFilterAttribute
{
//MVC's [HandleError] doesn't work (it returns a view)
public override void OnException(System.Web.Http.Filters.HttpActionExecutedContext actionExecutedContext)
{
//log it
Trace.TraceError(actionExecutedContext.Exception.ToString());
//handle it
actionExecutedContext.Response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
}
Then apply the attribute on action or controller:
[HandleExceptionAttribute]
public int Post(Order order)
Or into GlobalConfiguration filters in App_Start (NB: not MVC action filters!!)
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
config.Filters.Add(new HandleExceptionAttribute());
WebAPI 2.1 Loggers and Handlers
Global logging and handling is only available from WebApi 2.1 (January 2014+)
// There can be multiple exception loggers (by default there are 0)
config.Services.Add(typeof(System.Web.Http.ExceptionHandling.IExceptionLogger),
new MyExceptionLogger());
// There must be exactly 1 exception handler. (There is a default one that may be replaced.)
config.Services.Replace(typeof(System.Web.Http.ExceptionHandling.IExceptionHandler),
new MyExceptionHandler());
Implementations:
public class MyExceptionLogger : System.Web.Http.ExceptionHandling.ExceptionLogger
{
public override void Log(System.Web.Http.ExceptionHandling.ExceptionLoggerContext context)
{
Trace.TraceError("Method {0} url {1} exception {2}",
context.Request.Method,
context.Request.RequestUri,
context.Exception);
}
}
public class MyExceptionHandler : System.Web.Http.ExceptionHandling.ExceptionHandler
{
public override void Handle(System.Web.Http.ExceptionHandling.ExceptionHandlerContext context)
{
//return an IHttpActionResult
context.Result = new InternalServerErrorResult(context.Request);
}
}
Async Disconnect Errors (.net 4.5)
In .net 4.5 you can use async Task actions. Because they are async, disconnects trigger the standard escalation policy to terminate the process. Opps. Put this in Global.asax Application_Start:
//log and swallow the async disconnect errors "The remote host closed the connection. The error code is 0x80070057."
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
e.SetObserved();
Trace.TraceError(e.Exception.ToString());
};