In the article "Error handling in WCF based web applications" we've shown a custom error handler for RESTful service based on WCF. This time we shall do the same for Web API 2.1 service.
Web API 2.1 provides an elegant way to implementat custom error handlers/loggers, see the following article. Web API permits many error loggers followed by a single error handler for all uncaught exceptions. A default error handler knows to output an error both in XML and JSON formats depending on requested MIME type.
In our projects we use unique error reference IDs. This feature allows to an end-user to refer to any error that has happened during the application life time and pass such error ID to the technical support for further investigations. Thus, error details passed to the client-side contain an ErrorID field. An error logger generates ErrorID and passes it over to an error handler for serialization.
Let's look at our error handling implementation for a Web API application.
The first part is an implementation of IExceptionLogger interface. It assigns ErrorID and logs all errors:
IExceptionLogger
/// Defines a global logger for unhandled exceptions. public class GlobalExceptionLogger : ExceptionLogger { /// Writes log record to the database synchronously. public override void Log(ExceptionLoggerContext context) { try { var request = context.Request; var exception = context.Exception; var id = LogError( request.RequestUri.ToString(), context.RequestContext == null ? null : context.RequestContext.Principal.Identity.Name, request.ToString(), exception.Message, exception.StackTrace); // associates retrieved error ID with the current exception exception.Data["NesterovskyBros:id"] = id; } catch { // logger shouldn't throw an exception!!! } } // in the real life this method may store all relevant info into a database. private long LogError( string address, string userid, string request, string message, string stackTrace) { ... } }
The second part is the implementation of IExceptionHandler:
IExceptionHandler
/// Defines a global handler for unhandled exceptions. public class GlobalExceptionHandler : ExceptionHandler { /// This core method should implement custom error handling, if any. /// It determines how an exception will be serialized for client-side processing. public override void Handle(ExceptionHandlerContext context) { var requestContext = context.RequestContext; var config = requestContext.Configuration; context.Result = new ErrorResult( context.Exception, requestContext == null ? false : requestContext.IncludeErrorDetail, config.Services.GetContentNegotiator(), context.Request, config.Formatters); } /// An implementation of IHttpActionResult interface. private class ErrorResult : ExceptionResult { public ErrorResult( Exception exception, bool includeErrorDetail, IContentNegotiator negotiator, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters) : base(exception, includeErrorDetail, negotiator, request, formatters) { } /// Creates an HttpResponseMessage instance asynchronously. /// This method determines how a HttpResponseMessage content will look like. public override Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) { var content = new HttpError(Exception, IncludeErrorDetail); // define an additional content field with name "ErrorID" content.Add("ErrorID", Exception.Data["NesterovskyBros:id"] as long?); var result = ContentNegotiator.Negotiate(typeof(HttpError), Request, Formatters); var message = new HttpResponseMessage { RequestMessage = Request, StatusCode = result == null ? HttpStatusCode.NotAcceptable : HttpStatusCode.InternalServerError }; if (result != null) { try { // serializes the HttpError instance either to JSON or to XML // depend on requested by the client MIME type. message.Content = new ObjectContent<HttpError>( content, result.Formatter, result.MediaType); } catch { message.Dispose(); throw; } } return Task.FromResult(message); } } }
Last, but not least part of this solution is registration and configuration of the error logger/handler:
/// WebApi congiguation. public static class WebApiConfig { public static void Register(HttpConfiguration config) { ... // register the exception logger and handler config.Services.Add(typeof(IExceptionLogger), new GlobalExceptionLogger()); config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler()); // set error detail policy according with value from Web.config var customErrors = (CustomErrorsSection)ConfigurationManager.GetSection("system.web/customErrors"); if (customErrors != null) { switch (customErrors.Mode) { case CustomErrorsMode.RemoteOnly: { config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.LocalOnly; break; } case CustomErrorsMode.On: { config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Never; break; } case CustomErrorsMode.Off: { config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always; break; } default: { config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Default; break; } } } } }
The client-side error handler remain almost untouched. The implementation details you may find in /Scripts/api/api.js and Scripts/controls/error.js files.
/Scripts/api/api.js
Scripts/controls/error.js
You may download the demo project here. Feel free to use this solution in your .NET projects.