I am new to ASP.NET web api and I was reading these two article about exception handling
Exception handling
Error handling
While I understand Exception handling link, I am not sure about using Error handling. Here is what I want to do:
when an error occurs, I want to log that error with message, stack trace & request object that was sent with the original request.
The type of every incoming request can be different & I am planning to serialize the request object into JSON string and store them in DB while error logging.
It seems like I should add try catch block on every method and then perform the exception handling and logging. This will be a tedious task. Is there any way to globally handle error and still able to capture exceptions details (message, stack trace) & log request object.
Is there any way to globally handle error and still able to capture
exceptions details (message, stack trace) & log request object.
Yes, ASP.NET Web API 2.1 have framework support for global handling of unhandled exceptions, instead of adding try catch block on every method.
It allows use to customize the HTTP response that is sent when an unhandled application exception occurs.
WebApiConfig
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// ...
config.Services.Replace(typeof (IExceptionHandler),
new GlobalExceptionHandler());
}
}
GlobalExceptionHandler
public class GlobalExceptionHandler : ExceptionHandler
{
public override void Handle(ExceptionHandlerContext context)
{
var exception = context.Exception;
var httpException = exception as HttpException;
if (httpException != null)
{
context.Result = new CustomErrorResult(context.Request,
(HttpStatusCode) httpException.GetHttpCode(),
httpException.Message);
return;
}
// Return HttpStatusCode for other types of exception.
context.Result = new CustomErrorResult(context.Request,
HttpStatusCode.InternalServerError,
exception.Message);
}
}
CustomErrorResult
public class CustomErrorResult : IHttpActionResult
{
private readonly string _errorMessage;
private readonly HttpRequestMessage _requestMessage;
private readonly HttpStatusCode _statusCode;
public CustomErrorResult(HttpRequestMessage requestMessage,
HttpStatusCode statusCode, string errorMessage)
{
_requestMessage = requestMessage;
_statusCode = statusCode;
_errorMessage = errorMessage;
}
public Task<HttpResponseMessage> ExecuteAsync(
CancellationToken cancellationToken)
{
return Task.FromResult(_requestMessage.CreateErrorResponse(
_statusCode, _errorMessage));
}
}
Credit to ASP.NET Web API 2: Building a REST Service from Start to Finish
Create a filter to handle those operations for you and then register the filter globally. We do something very similar, here's the filter class we use.
public class FailedApiRequestLoggerAttribute : ActionFilterAttribute
{
private readonly bool _removeErrorDetailsFromResponse;
public FailedApiRequestLoggerAttribute(bool removeErrorDetailsFromResponse)
{ _removeErrorDetailsFromResponse = removeErrorDetailsFromResponse; }
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
base.OnActionExecuted(actionExecutedContext);
var log = LoggerFactory.GetLogger(actionExecutedContext.ActionContext.ControllerContext.Controller.GetType().Name);
// If there is no response object then we're probably here because an exception was
// thrown and thrown exceptions are handled elsewhere.
if (actionExecutedContext.Response?.IsSuccessStatusCode == false)
{
var error = new StringBuilder();
error.AppendLine("API Call Returned Non-Success Status");
error.AppendLine($"{actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerType.FullName}.{actionExecutedContext.ActionContext.ActionDescriptor.ActionName}");
if (actionExecutedContext.ActionContext.ActionArguments.Any())
{ error.AppendLine($" Arguments"); }
foreach (var argument in actionExecutedContext.ActionContext.ActionArguments)
{ error.AppendLine($" {JsonConvert.SerializeObject(argument)}"); }
error.AppendLine(" Response");
error.AppendLine($" Status Code: {actionExecutedContext.Response.StatusCode}; Reason: {actionExecutedContext.Response.ReasonPhrase}");
var content = actionExecutedContext.Response.Content as ObjectContent<HttpError>;
if (content != null)
{
error.AppendLine($" {JsonConvert.SerializeObject(content.Value)}");
if (_removeErrorDetailsFromResponse)
{ ((HttpError)content.Value).Clear(); }
}
log.Warning(error.ToString());
}
}
}
And then it gets registered globally.
config.Filters.Add(new FailedApiRequestLoggerAttribute(true));
Related
Should catching exceptions be part of the business logic such as the Service layer, or should they be caught in the controllers' methods?
For example:
Controller UpdateUser method
[HttpPut]
[Route("{id}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<UserDto>> UpdateUserInfo(int id, UserDto userRequest)
{
try
{
var user = _userMapper.ConvertToEntity(userRequest);
var updatedUser = await _userService.UpdateAsync(user, id);
var result = _userMapper.ConvertToUserDto(updatedUser);
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError("Exception caught attempting to update user - Type: {ex}", ex.GetType());
_logger.LogError("Message: {ex}", ex.Message);
return StatusCode(500, ex.Message);
}
}
The Service Layer
public async Task<User> UpdateAsync(User user, int id)
{
await _repository.UpdateAsync(user, id);
return user;
}
So, should the exceptions be caught in the service layer or the controller? Or is it subjective?
It's dependent on the business of your application. maybe in your service you should use a try/catch block to adding a log or do anything when exception occurred. but usually I use a global try/catch in a middleware to get exception and send correct response to the client.
public class AdvancedExceptionHandler
{
private readonly RequestDelegate _next;
private readonly ILogger<AdvancedExceptionHandler> _logger;
private readonly IWebHostEnvironment _env;
public AdvancedExceptionHandler(RequestDelegate next, ILogger<AdvancedExceptionHandler> logger, IWebHostEnvironment env)
{
_next = next;
_logger = logger;
_env = env;
}
public async Task Invoke(HttpContext context)
{
string message = null;
HttpStatusCode httpStatusCode = HttpStatusCode.InternalServerError;
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex.Message, ex);
if (_env.IsDevelopment())
{
var dic = new Dictionary<string, string>
{
["StackTrace"] = ex.StackTrace,
["Exception"] = ex.Message
};
message = JsonConvert.SerializeObject(dic);
}
else
{
message = "an error has occurred";
}
await WriteToReponseAsync();
}
async Task WriteToReponseAsync()
{
if (context.Response.HasStarted)
throw new InvalidOperationException("The response has already started");
var exceptionResult = new ExceptionResult(message, (int)httpStatusCode);
var result = JsonConvert.SerializeObject(exceptionResult);
context.Response.StatusCode = (int)httpStatusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}
}
}
ExceptionResutl class:
public class ExceptionResult
{
public ExceptionResult(string message, int statusCode)
{
this.Message = message;
this.StatusCode = statusCode;
}
public string Message { get; set; }
public int StatusCode { get; set; }
}
public static class ExceptionHandlerMiddlewareExtension
{
public static void UseAdvancedExceptionHandler(this IApplicationBuilder app)
{
app.UseMiddleware<AdvancedExceptionHandler>();
}
}
Then adding middleware in Configure method
public void Configure(IApplicationBuilder app)
{
app.UseAdvancedExceptionHandler();//<--NOTE THIS
}
I don't use try/catch block in controllers. (my opinion)
Catching exceptions in your controller will quickly start to violate some clean code principles, like DRY.
If I understand correctly, the example you have written is that you want to log some errors in case any exceptions are thrown in your code. This is reasonable, but if you begin to add more endpoints, you'll notice you have the same try/catch in all your controller methods. The best way to refactor this is to use a middleware that will catch the exception and map it to a response that you want.
Over time as you begin to update your application to have more features you may have a situation where multiple endpoints are throwing similar errors and you want it to be handled in a similar way. For example, in your example, if the user doesn't exist, the application (in your service layer) may throw an UserNotFoundException, and you may have some other endpoints which can throw the same error, too.
You could create another middleware to handle this or even extend your existing middleware.
One of the better approaches I've seen over the years is to use this library https://github.com/khellang/Middleware/tree/master/src/ProblemDetails to handle the boiler plate for you.
I have an API as follows(Mock API), where I am pushing the exception in the catch block to a Central logging Portal. Again in the Exception Filter I am phrasing the Exception Message & throwing the message to User.
[customeFilter]
Public asyc Task<IactionResult> DoTask()
{
try
{
//sample Code
}
catch(exception ex)
{
_log.LogError(ex);
}
public class customeFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
HttpStatusCode? httpErrorCode = HttpStatusCode.BadRequest;
if (context.Exception is DivideByZeroException dvd)
{
context.Result = new ObjectResult(dvd.Data)
{
StatusCode = (int?)HttpStatusCode.InternalServerError,
Value = "There is an issue with the data"
};
context.ExceptionHandled = true;
}
}
}
Issue: I want to keep the same Guid to both systems, i.e., to the central Logging & to the User via Exception Filter. I tried some approach to pass the information to Exception Filter, but didn't have any success with that. Even tried to find any unique identifier in the Exception object, but didn't find any.
Note:
I have a thought about Pushing the exception Log in the Exception Filter instead of Catch block. But I don't want to go with Static Class/Methods.
Do you want to track your user and his/her corresponding exception?
If you wan't my opinion, you don't need to generate a guid for this goal. You can use _httpContextAccessor.HttpContext.TraceIdentifier
Just inject your IHttpContextAccessor inside the filter and use TraceIdentifier.
For more clarification you can set that trace identifier in header of response you can do something like this:
public class CustomFilterAttribute : ExceptionFilterAttribute
{
private readonly IHttpContextAccessor _httpContextAccessor;
public CustomFilterAttribute(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public override void OnException(ExceptionContext context)
{
HttpStatusCode? httpErrorCode = HttpStatusCode.BadRequest;
if (context.Exception?.InnerException is DivideByZeroException dvd)
{
string uid = _httpContextAccessor.HttpContext.TraceIdentifier;
//logger.log(uid + exception).
_httpContextAccessor.HttpContext.Response.Headers.Add("tracking-uid", uid);
context.Result = new ObjectResult(dvd.Data)
{
StatusCode = (int?)HttpStatusCode.InternalServerError,
Value = "There is an issue with the data"
};
context.ExceptionHandled = true;
}
}
}
We installed an Exception filter
public class APIErrorHandlerAttribute : ExceptionFilterAttribute, IExceptionFilter
{
// Sets up log4net logger
private static readonly log4net.ILog log = log4net.LogManager.GetLogger
(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is BusinessException)
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent(context.Exception.Message),
ReasonPhrase = "Exception"
});
}
//Log Critical errors
log.Info("/****************** API Begin Error ******************/");
log.Info("Exception:" + context.Exception + " Case:" + CaseHelper.GetCurrentCaseID() + " UserID:" + UserHelper.GetCurrentUserID());
log.Info("/****************** API End Error ******************/");
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent(string.Format("Message: {0}\nStack Trace: {1}", context.Exception.Message, context.Exception.StackTrace)),
ReasonPhrase = "Critical Exception",
});
}
}
for all WebApi routes
//*** Log API Error Globally using Log4net
public static void RegisterFilters(HttpConfiguration config)
{
config.Routes.MapHttpRoute("DefaultApiError", "api/{controller}/{action}/{id}", new { id = RouteParameter.Optional });
config.Filters.Add(new LRAPIErrorHandlerAttribute());
}
We also have an authorization attribute that we attach to some controllers. It looks something like this:
public class LegalRadiusAPIAuthorizationAttribute : ActionFilterAttribute, IActionFilter, IFilter
{
public string Permissions { get; set; }
public string Roles { get; set; }
public string Users { get; set; }
public override void OnActionExecuted(HttpActionExecutedContext filterContext)
{
if (!HttpContext.Current.User.Identity.IsAuthenticated)
{
throw new HttpResponseException(HttpStatusCode.NonAuthoritativeInformation); //203
}
}
}
So, if an unauthenticated user hits a route that requires him to be logged in, we throw. If I debug inside this filter and open up the exception dialog I can see the following properties on the exception object:
If I look at the Response prop I see all the pertinent details of the exception thrown.
The problem is when this exception gets handled by APIErrorHandlerAttribute. By the time the exception gets to the handler I can't seem to find the Response property of the exception in context.Exception...
Even though, at the handler I get the following error message from the caught exception:
Processing of the HTTP request resulted in an exception. Please see the HTTP response returned by the 'Response' property of this exception for details.
implying that the exception in context should have that property.
I feel like I'm having scope issues and that the original exception object that was thrown in the filter isn't in the context parameter of the Exception Handler.
You just need to access the [System.Web.Http.HttpResponseException] from your context.Exception. In order to do that, and based on the picture, you need to right click on the [System.Web.Http.HttpResponseException] and add a watcher on it. That is going to give you the appropriate casting in order to retrieve the information from the context.
Try this
var exceptionData = new ExceptionData
{
Name = "exception name",
Message = "exception message"
};
Content = new ObjectContent<ExceptionData>(exceptionData, JsonFormatter),
I have a global exception filter named LogErrorAttribute:
public class LogErrorAttribute : IExceptionFilter
{
private ILogUtils logUtils;
public void OnException(ExceptionContext filterContext)
{
if (this.logUtils == null)
{
this.logUtils = StructureMapConfig.Container.GetInstance<ILogUtils>();
}
this.logUtils.LogError(HttpContext.Current.User.Identity.GetUserId(), "Unknown error.", filterContext.Exception);
}
}
It's registered along with the standard HandleErrorAttribute filter:
filters.Add(new LogErrorAttribute());
filters.Add(new HandleErrorAttribute());
I'm registering those filters like this:
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
I also have an Application_Error fallback:
protected void Application_Error()
{
var exception = Server.GetLastError();
Server.ClearError();
var httpException = exception as HttpException;
//Logging goes here
var routeData = new RouteData();
routeData.Values["controller"] = "Error";
routeData.Values["action"] = "Index";
if (httpException != null)
{
if (httpException.GetHttpCode() == 404)
{
routeData.Values["action"] = "NotFound";
}
Response.StatusCode = httpException.GetHttpCode();
}
else
{
Response.StatusCode = 500;
}
// Avoid IIS7 getting involved
Response.TrySkipIisCustomErrors = true;
// Execute the error controller
if (exception != null)
{
this.errorLogger.Log(LogLevel.Error, "An unknown exception has occurred.", exception);
}
else if (httpException != null)
{
this.errorLogger.Log(LogLevel.Error, "An unknown HTTP exception has occurred.", httpException);
}
else
{
this.errorLogger.Log(LogLevel.Error, "An unknown error has occurred.");
}
}
Now, I have an API controller that grabs some data from the database and then uses AutoMapper to map the models to view models:
var viewModels = AutoMapper.Mapper.Map(users, new List<UserViewModel>());
Inside that AutoMapper configuration a custom resolver executes for one of the properties:
var appModuleAssignments = this.appModuleAssignmentManager.Get(userId);
var appModules = appModuleAssignments.Select(x => this.appModuleManager.Get(x.AppModuleId));
return AutoMapper.Mapper.Map(appModules, new List<AppModuleViewModel>());
At the moment I'm forcing the appModuleManager.Get statement to throw a regular exception:
throw new Exception("Testing global filter.");
This subsequently throws an exception in AutoMapper, both of which are unhandled, however neither the global filter or the Application_Error are picking up this exception.
What did I do wrong here?
A couple things I have done since posting:
Added the customErrors attribute to the Web.config to turn them on.
Removed the HandleErrorAttribute global filter because I realized it was setting the error to handled if it were even running. I would not expect it to be executing anyway because this error occurs outside the controller, but it would have likely bit me later.
The short answer is that you are adding a MVC Exception Filter rather than a Web API Exception Filter.
Your implementation checks for ExceptionContext rather than HttpActionExecutedContext
public override void OnException(HttpActionExecutedContext actionExecutedContext)
Since the framework will raises a Http Exception rather than a MVC Exception, your OnException override method is not triggered.
So, a more complete example:
public class CustomExceptionFilter : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext actionExecutedContext)
{
message = "Web API Error";
status = HttpStatusCode.InternalServerError;
actionExecutedContext.Response = new HttpResponseMessage()
{
Content = new StringContent(message, System.Text.Encoding.UTF8, "text/plain"),
StatusCode = status
};
base.OnException(actionExecutedContext);
}
}
Another important step is to register your Global Web API Exception Filter in WebApiConfig.cs, in the Register(HttpConfiguration config) method.
public static void Register(HttpConfiguration config)
{
...
config.Filters.Add(new CustomExceptionFilter());
}
Dave Alperovich answer will solve your issue by using HttpActionExecutedContext
public override void OnException(HttpActionExecutedContext context)
However as you are trying to capture all possible exceptions your application might generate then apart from exception filters one should use message handlers as well. A detailed explanation can be find here - http://www.asp.net/web-api/overview/error-handling/web-api-global-error-handling.
In summary, there are a number of cases that exception filters can’t handle. For example:
Exceptions thrown from controller constructors.
Exceptions thrown from message handlers.
Exceptions thrown during routing.
Exceptions thrown during response content serialization
So, If an unhandled error occurs from anywhere within the application, your Exception Handler will catch it and allow you to take specific action.
//Global exception handler that will be used to catch any error
public class MyExceptionHandler : ExceptionHandler
{
private class ErrorInformation
{
public string Message { get; set; }
public DateTime ErrorDate { get; set; }
}
public override void Handle(ExceptionHandlerContext context)
{
context.Result = new ResponseMessageResult(context.Request.CreateResponse(HttpStatusCode.InternalServerError,
new ErrorInformation { Message="An unexpected error occured. Please try again later.", ErrorDate=DateTime.UtcNow }));
}
}
In ASP.NET MVC 5 you could throw a HttpException with a HTTP code and this would set the response like so:
throw new HttpException((int)HttpStatusCode.BadRequest, "Bad Request.");
HttpException does not exist in ASP.NET Core. What is the equivalent code?
I implemented my own HttpException and supporting middleware which catches all HttpException's and turns them into the corresponding error response. A short extract can be seen below. You can also use the Boxed.AspNetCore Nuget package.
Usage Example in Startup.cs
public void Configure(IApplicationBuilder application)
{
application.UseIISPlatformHandler();
application.UseStatusCodePagesWithReExecute("/error/{0}");
application.UseHttpException();
application.UseMvc();
}
Extension Method
public static class ApplicationBuilderExtensions
{
public static IApplicationBuilder UseHttpException(this IApplicationBuilder application)
{
return application.UseMiddleware<HttpExceptionMiddleware>();
}
}
Middleware
internal class HttpExceptionMiddleware
{
private readonly RequestDelegate next;
public HttpExceptionMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await this.next.Invoke(context);
}
catch (HttpException httpException)
{
context.Response.StatusCode = httpException.StatusCode;
var responseFeature = context.Features.Get<IHttpResponseFeature>();
responseFeature.ReasonPhrase = httpException.Message;
}
}
}
HttpException
public class HttpException : Exception
{
private readonly int httpStatusCode;
public HttpException(int httpStatusCode)
{
this.httpStatusCode = httpStatusCode;
}
public HttpException(HttpStatusCode httpStatusCode)
{
this.httpStatusCode = (int)httpStatusCode;
}
public HttpException(int httpStatusCode, string message) : base(message)
{
this.httpStatusCode = httpStatusCode;
}
public HttpException(HttpStatusCode httpStatusCode, string message) : base(message)
{
this.httpStatusCode = (int)httpStatusCode;
}
public HttpException(int httpStatusCode, string message, Exception inner) : base(message, inner)
{
this.httpStatusCode = httpStatusCode;
}
public HttpException(HttpStatusCode httpStatusCode, string message, Exception inner) : base(message, inner)
{
this.httpStatusCode = (int)httpStatusCode;
}
public int StatusCode { get { return this.httpStatusCode; } }
}
In the long term, I would advise against using exceptions for returning errors. Exceptions are slower than just returning an error from a method.
After a brief chat with #davidfowl, it seems that ASP.NET 5 has no such notion of HttpException or HttpResponseException that "magically" turn to response messages.
What you can do, is hook into the ASP.NET 5 pipeline via MiddleWare, and create one that handles the exceptions for you.
Here is an example from the source code of their error handler middleware which will set the response status code to 500 in case of an exception further up the pipeline:
public class ErrorHandlerMiddleware
{
private readonly RequestDelegate _next;
private readonly ErrorHandlerOptions _options;
private readonly ILogger _logger;
public ErrorHandlerMiddleware(RequestDelegate next,
ILoggerFactory loggerFactory,
ErrorHandlerOptions options)
{
_next = next;
_options = options;
_logger = loggerFactory.CreateLogger<ErrorHandlerMiddleware>();
if (_options.ErrorHandler == null)
{
_options.ErrorHandler = _next;
}
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError("An unhandled exception has occurred: " + ex.Message, ex);
if (context.Response.HasStarted)
{
_logger.LogWarning("The response has already started,
the error handler will not be executed.");
throw;
}
PathString originalPath = context.Request.Path;
if (_options.ErrorHandlingPath.HasValue)
{
context.Request.Path = _options.ErrorHandlingPath;
}
try
{
var errorHandlerFeature = new ErrorHandlerFeature()
{
Error = ex,
};
context.SetFeature<IErrorHandlerFeature>(errorHandlerFeature);
context.Response.StatusCode = 500;
context.Response.Headers.Clear();
await _options.ErrorHandler(context);
return;
}
catch (Exception ex2)
{
_logger.LogError("An exception was thrown attempting
to execute the error handler.", ex2);
}
finally
{
context.Request.Path = originalPath;
}
throw; // Re-throw the original if we couldn't handle it
}
}
}
And you need to register it with StartUp.cs:
public class Startup
{
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerfactory)
{
app.UseMiddleWare<ExceptionHandlerMiddleware>();
}
}
Alternatively, if you just want to return an arbitrary status code and aren't concerned with the Exception-based approach, you can use
return new HttpStatusCodeResult(400);
Update: as of .NET Core RC 2, the Http prefix is dropped. It is now:
return new StatusCodeResult(400);
The Microsoft.AspNet.Mvc.Controller base class exposes a HttpBadRequest(string) overload which takes an error message to return to the client. So from within a controller action, you could call:
return HttpBadRequest("Bad Request.");
Ultimately my nose says any private methods called from within a controller action should either be fully http-context-aware and return an IActionResult, or perform some other small task completely isolated from the fact that it's inside of an http pipeline. Granted this is my personal opinion, but a class that performs some piece of business logic should not be returning HTTP status codes, and instead should be throwing its own exceptions which can be caught and translated at the controller/action level.
There is no equivalent in ASP.NET Core itself. As others have said, the way to implement this is with a middleware and your own exceptions.
The Opw.HttpExceptions.AspNetCore NuGet package does exactly this.
Middleware and extensions for returning exceptions over HTTP, e.g. as ASP.NET Core Problem Details. Problem Details are a machine-readable format for specifying errors in HTTP API responses based on https://www.rfc-editor.org/rfc/rfc7807. But you are not limited to returning exception results as Problem Details, but you can create your own mappers for your own custom formats.
It is configurable and well documented.
Here is the list of provided exceptions out of the box:
4xx
400 BadRequestException
400 InvalidModelException
400 ValidationErrorException<T>
400 InvalidFileException
401 UnauthorizedException
403 ForbiddenException
404 NotFoundException
404 NotFoundException<T>
409 ConflictException
409 ProtectedException
415 UnsupportedMediaTypeException
5xx
500 InternalServerErrorException
500 DbErrorException
500 SerializationErrorException
503 ServiceUnavailableException
Here is an extended version of #muhammad-rehan-saeed answer.
It logs exceptions conditionaly and disables http cache.
If you use this and UseDeveloperExceptionPage, you should call UseDeveloperExceptionPage before this.
Startup.cs:
app.UseMiddleware<HttpExceptionMiddleware>();
HttpExceptionMiddleware.cs
/**
* Error handling: throw HTTPException(s) in business logic, generate correct response with correct httpStatusCode + short error messages.
* If the exception is a server error (status 5XX), this exception is logged.
*/
internal class HttpExceptionMiddleware
{
private readonly RequestDelegate next;
public HttpExceptionMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await this.next.Invoke(context);
}
catch (HttpException e)
{
var response = context.Response;
if (response.HasStarted)
{
throw;
}
int statusCode = (int) e.StatusCode;
if (statusCode >= 500 && statusCode <= 599)
{
logger.LogError(e, "Server exception");
}
response.Clear();
response.StatusCode = statusCode;
response.ContentType = "application/json; charset=utf-8";
response.Headers[HeaderNames.CacheControl] = "no-cache";
response.Headers[HeaderNames.Pragma] = "no-cache";
response.Headers[HeaderNames.Expires] = "-1";
response.Headers.Remove(HeaderNames.ETag);
var bodyObj = new {
Message = e.BaseMessage,
Status = e.StatusCode.ToString()
};
var body = JsonSerializer.Serialize(bodyObj);
await context.Response.WriteAsync(body);
}
}
}
HTTPException.cs
public class HttpException : Exception
{
public HttpStatusCode StatusCode { get; }
public HttpException(HttpStatusCode statusCode)
{
this.StatusCode = statusCode;
}
public HttpException(int httpStatusCode)
: this((HttpStatusCode) httpStatusCode)
{
}
public HttpException(HttpStatusCode statusCode, string message)
: base(message)
{
this.StatusCode = statusCode;
}
public HttpException(int httpStatusCode, string message)
: this((HttpStatusCode) httpStatusCode, message)
{
}
public HttpException(HttpStatusCode statusCode, string message, Exception inner)
: base(message, inner)
{
}
public HttpException(int httpStatusCode, string message, Exception inner)
: this((HttpStatusCode) httpStatusCode, message, inner)
{
}
}
I had better results with this code than with :
UseExceptionHandler:
automatically logs every "normal" exceptions (ex 404).
disabled in dev mode (when app.UseDeveloperExceptionPage is called)
cannot catch only specific exceptions
Opw.HttpExceptions.AspNetCore: logs exception when everything works fine
See also ASP.NET Core Web API exception handling
Starting from ASP.NET Core 3 you can use ActionResult to return HTTP status code:
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<ITEMS_TYPE> GetByItemId(int id)
{
...
if (result == null)
{
return NotFound();
}
return Ok(result);
}
More details are here: https://learn.microsoft.com/en-us/aspnet/core/web-api/action-return-types?view=aspnetcore-3.1