I am developing WebAPI and want to catch all my ApiException custom exceptions and display WebAPI-friendly responses. The ApiException exception can be thrown from Action or Filter like IAuthorizationFilter or ActionFilterAttribute.
First I tried to use IExceptionFilter but later I found that the IExceptionFilter handles only exceptions thrown from Actions and not from other Filters.
public class ApiExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
var exception = context.Exception;
if (exception is not ApiException responseException)
{
responseException = new ApiException(ResponseMessageType.UnhandledException);
}
context.Result = new ObjectResult(new ResultMessageDto(responseException))
{
StatusCode = responseException.HttpStatusCode
};
}
}
The second approach that I found many suggest to use is the Middleware but this is not the correct way by WebAPI design.
public class ErrorHandlerMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlerMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception exception)
{
var response = context.Response;
response.ContentType = "application/json";
if (exception is not ApiException responseException)
{
responseException = new ApiException(ResponseMessageType.UnhandledException);
}
response.StatusCode = responseException.HttpStatusCode;
await response.WriteAsJsonAsync(new ResultMessageDto(responseException), new JsonSerializerOptions()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = null
});
}
}
}
The Middleware exception handling skips WebAPI MVC OutputFormaters and responds only in JSON or what is set by the developer. This solution is bad by design because do not respect Accept header.
How to handle Exceptions in Actions and Filters without leaving MVC scope?
I feel it is best to handle in Middleware, Here is a sample code. I am using Json but over here, you can use any output formatter (even custom ones).
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
int statusCode = StatusCodes.Status500InternalServerError;
string exceptionType, exceptionDetail;
context.Response.ContentType = "application/json";
switch (exception)
{
case ArgumentNullException:
exceptionType = "Argument Null Exception";
exceptionDetail = exception.Message;
break;
case ApiException:
exceptionType = "API Exception";
exceptionDetail = exception.Message;
break;
default:
exceptionType = "Unhandled"
exceptionDetail = "Something went wrong";
break;
}
var problemDetails = new Microsoft.AspNetCore.Mvc.ProblemDetails
{
Status = statusCode,
Type = exceptionType,
Title = "An error has occurred within the PromComm Application",
Detail = exceptionDetail,
Instance = context.Request.Path
};
// You can use any formatter here even with custom messages
var problemDetailsJson = System.Text.Json.JsonSerializer.Serialize(problemDetails);
context.Response.StatusCode = statusCode;
await context.Response.WriteAsync(problemDetailsJson);
}
Related
I develop a .NET Core 3 REST API using C#.
I read a lot about this topic, and I implemented a custom exception handler middleware, which is work well.
But I realized that the error messages are structured differently.
For example, I use .Net Core Identity and when I try to save a username which is already saved, then I get an HTTP 400 with the following error response: (Sorry about that, I can't post images.)
error Array(1){
0: {
code: "...",
description: ".."
}
}
And, I use too the Attribute validation, which gives this HTTPErrorResponse:
error: {
.
.
.
errors:{
Password: Array(1) {
0: "The field..."
}
}
}
Finally, my custom exception handler class gives an ErroDTO which contains a Code, and a description.
So the problem is there are 3 things, which give 3 different error responses, and I don't know how can I handle this, in Frontend which is an Angular project.
I would like to handle all errors and exceptions by my ErroDto class, but I don't know how to transform the Identity or Attribute errors.
I think that I could test the email and password validation directly in the endpoint instead of using Attribute, and I could test if the existing email, and the Identity won't be throw error. But I think it is not the best practice, and it will be a lot of boilerplate code.
This is my exception handling class:
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception ex)
{
var code = HttpStatusCode.InternalServerError; // 500 if unexpected
ErrorDto error = new ErrorDto();
if (ex is WrongCredentialsException)
{
code = HttpStatusCode.OK;
WrongCredentialsException wrongCredentialsException = (WrongCredentialsException) ex;
error.Code = wrongCredentialsException.Code;
error.Description = wrongCredentialsException.Message;
}
var result = JsonConvert.SerializeObject(new { error });
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
return context.Response.WriteAsync(result);
}
}
Create custom Exception class
using System;
[Serializable]
public class EmployeeMgmtException : Exception
{
public EmployeeMgmtException(string message) : base(message)
{
}
}
Create an Exception Middleware class
public class ExceptionMiddleware
{
private readonly RequestDelegate next;
private readonly ILogger<ExceptionMiddleware> logger;
private readonly ResourceManager resourceManager;
}
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware>
logger)
{
this.next = next;
this.logger = logger;
this.resourceManager = new
ResourceManager("EmployeeException.API.Resources.Resource",
typeof(ExceptionMiddleware).Assembly);
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await this.next.Invoke(httpContext);
}
catch (Exception ex)
{
await this.HandleExceptionAsync(httpContext, ex);
}
}
private Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
ErrorResponse response = new ErrorResponse();
if (exception is EmployeeMgmtException)
{
context.Response.StatusCode = (int)HttpStatusCode.OK;
response.ErrorMessage = exception.Message;
}
else
{
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
response.ErrorMessage = this.resourceManager.GetString("FailedToProcess",
CultureInfo.CurrentUICulture);
}
this.logger.LogCritical(exception, exception.Message, GetProperties(context));
return context.Response.WriteAsync(response.ToString());
}
}
Register Exception Middleware filter in Startup.cs
app.UseMiddleware<ExceptionMiddleware>();
I have this ErrorHandlingMiddleware that looks like this:
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate _next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
this._next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception ex)
{
var statusCode = (int)HttpStatusCode.InternalServerError;
if (ex is NotFoundError) statusCode = (int)HttpStatusCode.NotFound;
//else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
//else if (ex is MyException) code = HttpStatusCode.BadRequest;
var error = new AttemptError(statusCode, ex.Message, ex);
context.Response.ContentType = "application/json";
context.Response.StatusCode = statusCode;
return context.Response.WriteAsync(error.ToString());
}
}
And I have added this to my Startup class:
public void Configure(IApplicationBuilder app)
{
app.UseMiddleware<ErrorHandlingMiddleware>();
app.SeedIdentityServerDatabase();
app.UseDeveloperExceptionPage();
app.UseIdentityServer();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "r3plica Identity Server v1");
c.OAuthClientId("swagger");
c.OAuthAppName("Swagger Api UI");
});
app.UseMvc();
}
I would expect that if I was anywhere in my application and I throw an exception, it would be caught and it would execute this line:
await HandleExceptionAsync(context, ex);
So, I set up a test:
throw new Exception();
Which is thrown in my controller. When I run my application and then call the endpoint that has that exception thrown, it does indeed get to the Invoke method of my ErrorHandlingMiddleware, but instead of an exception being caught, it just goes to the await _next(context)....
Does anyone know what I am doing wrong?
Your middleware will work if you place the call to use it after app.UseDeveloperExceptionPage(); in Startup.
Handle errors in ASP.NET Core says
Place the call to UseDeveloperExceptionPage before any middleware that you want to catch exceptions.
I fixed this by creating a filter:
public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter
{
public int Order { get; set; } = int.MaxValue - 10;
public void OnActionExecuting(ActionExecutingContext context) { }
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.Exception == null) return;
var attempt = Attempt<string>.Fail(context.Exception);
if (context.Exception is AttemptException exception)
{
context.Result = new ObjectResult(attempt)
{
StatusCode = exception.StatusCode,
};
}
else
{
context.Result = new ObjectResult(attempt)
{
StatusCode = (int)HttpStatusCode.InternalServerError,
};
}
context.ExceptionHandled = true;
}
}
And registering it like this:
services.AddControllers(options => options.Filters.Add(new HttpResponseExceptionFilter()));
I am playing around with the powers of middleware in ASP.NET Core.
I replaced all my try/catch statements in favor for a global exception handling middleware.
It has the tasks to handle all uncaught exceptions (log them, return 500 Internal Server Error response) but should also handle all manually created results with status code 500.
I am testing against this controller:
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet()]
public ActionResult<string> Get()
{
return "hello";
}
[HttpPost("{id:int}")]
public IActionResult Post(int id)
{
throw new Exception("test exception");
}
[HttpPut("{id:int}")]
public IActionResult Put(int id)
{
return StatusCode(500, "this information may contains sensitive data");
}
}
The skeleton of my middleware so far looks like:
public class ExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionMiddleware> _logger;
private LogLevel LogLevel { get; } = LogLevel.Error;
private bool EnforceEmptyResult { get; } = true;
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_next = next ?? throw new ArgumentNullException(nameof(next));
}
public async Task InvokeAsync(HttpContext httpContext)
{
try
{
await _next(httpContext);
// handles the internal server error responses
if (httpContext.Response.StatusCode == (int)HttpStatusCode.InternalServerError)
{
await LogError(httpContext, LogLevel);
//if (EnforceEmptyResult) await ClearResponseBody(httpContext.Response);
}
}
catch (Exception ex)
{
// handles the uncaught exceptions
await LogException(httpContext, ex, LogLevel);
await HandleExceptionAsync(httpContext, (!EnforceEmptyResult) ? ex : null);
}
}
public Task HandleExceptionAsync(HttpContext context, Exception exception)
{
if (exception == null)
{
// the basic error response (500, no body, no content type)
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = null;
return context.Response.WriteAsync(string.Empty);
}
else
{
// the error response with the exception as plain text
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "text/plain";
return context.Response.WriteAsync(exception.ToString());
}
}
public async Task LogException(HttpContext context, Exception exception, LogLevel logLevel)
{
StringBuilder sb = new StringBuilder();
sb.Append("exception occured");
sb.Append(Environment.NewLine);
sb.Append("request body");
sb.Append(Environment.NewLine);
sb.Append(await RequestBody(context.Request));
sb.Append(Environment.NewLine);
sb.Append("---");
sb.Append(Environment.NewLine);
sb.Append("response body");
sb.Append(Environment.NewLine);
sb.Append(await ResponseBody(context.Response));
sb.Append(Environment.NewLine);
sb.Append("---");
_logger.Log(logLevel, exception, sb.ToString());
}
public async Task LogError(HttpContext context, LogLevel logLevel)
{
StringBuilder sb = new StringBuilder();
sb.Append("error occured");
sb.Append(Environment.NewLine);
sb.Append("request body");
sb.Append(Environment.NewLine);
sb.Append(await RequestBody(context.Request));
sb.Append(Environment.NewLine);
sb.Append("---");
sb.Append(Environment.NewLine);
sb.Append("response body");
sb.Append(Environment.NewLine);
sb.Append(await ResponseBody(context.Response));
sb.Append(Environment.NewLine);
sb.Append("---");
_logger.Log(logLevel, sb.ToString());
}
public async Task<string> RequestBody(HttpRequest request)
{
if (!request.ContentLength.HasValue || request.ContentLength.Value <= 0) return null;
var requestBody = request.Body;
request.EnableRewind();
byte[] buffer = new byte[request.ContentLength.Value];
await request.Body.ReadAsync(buffer, 0, buffer.Length);
request.Body = requestBody;
return Encoding.UTF8.GetString(buffer);
}
public async Task<string> ResponseBody(HttpResponse response)
{
if (!response.ContentLength.HasValue || response.ContentLength.Value <= 0) return null;
byte[] buffer = new byte[response.ContentLength.Value];
await response.Body.ReadAsync(buffer, 0, buffer.Length);
return Encoding.UTF8.GetString(buffer);
}
public async Task ClearResponseBody(HttpResponse response)
{
response.ContentType = null;
await response.WriteAsync(string.Empty);
}
}
I have a few issues with it relating to the handling of the bodies of the request/response.
In public async Task<string> RequestBody(HttpRequest request) I am using EnableRewind because a tutorial suggested it. I am not sure if I need it or not in that case. Fortunately so far it works and I see the body in the logs.
Am I using it correcly?
The public async Task<string> ResponseBody(HttpResponse response) seems to not work, since I newer see the response body. I want to display what is included in the response so far (may there are partial results) or in the case of returning status code 500 with an error message I want to log this error message.
How do I get the response body?
Using the public async Task ClearResponseBody(HttpResponse response) method throws an exception:
System.InvalidOperationException: The response headers cannot be modified because the response has already started.
How to manipulate the body of the response to avoid leaking of may sensitive development data?
And a minor detail on the side is that it seems I use the exception middleware in the wrong spot within the pipeline. I added it as the first entry right on the top of the public void Configure(IApplicationBuilder app, IHostingEnvironment env) method.
This seems to be potentionally wrong because in case of an uncaught exception I do not only get the logs of my middleware but also logs like:
Exception thrown: 'System.Exception' in Sample2.dll
An exception of type 'System.Exception' occurred in Sample2.dll but was not handled in user code
test exception
I am trying to create a middleware that can log the response body as well as manage exception globally and I was succeeded about that. My problem is that the custom message that I put on exception it's not showing on the response.
Middleware Code 01:
public async Task Invoke(HttpContext context)
{
context.Request.EnableRewind();
var originalBodyStream = context.Response.Body;
using (var responseBody = new MemoryStream())
{
try
{
context.Response.Body = responseBody;
await next(context);
context.Response.Body.Seek(0, SeekOrigin.Begin);
var response = await new StreamReader(context.Response.Body).ReadToEndAsync();
context.Response.Body.Seek(0, SeekOrigin.Begin);
// Process log
var log = new LogMetadata();
log.RequestMethod = context.Request.Method;
log.RequestUri = context.Request.Path.ToString();
log.ResponseStatusCode = context.Response.StatusCode;
log.ResponseTimestamp = DateTime.Now;
log.ResponseContentType = context.Response.ContentType;
log.ResponseContent = response;
// Keep Log to text file
CustomLogger.WriteLog(log);
await responseBody.CopyToAsync(originalBodyStream);
}
catch (Exception ex)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var jsonObject = JsonConvert.SerializeObject(My Custom Model);
await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
return;
}
}
}
If I write my middleware like that, my custom exception is working fine but I unable to log my response body.
Middleware Code 02:
public async Task Invoke(HttpContext context)
{
context.Request.EnableRewind();
try
{
await next(context);
}
catch (Exception ex)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var jsonObject = JsonConvert.SerializeObject(My Custom Model);
await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
return;
}
}
My Controller Action :
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
throw new Exception("Exception Message");
}
Now I want to show my exception message with my middleware 01, but it doesn't work but its work on my middleware 02.
So my observation is the problem is occurring for reading the context response. Is there anything I have missed in my middleware 01 code?
Is there any better way to serve my purpose that log the response body as well as manage exception globally?
I think what you are saying is that this code isn't sending it's response to the client.
catch (Exception ex)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var jsonObject = JsonConvert.SerializeObject(My Custom Model);
await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
return;
}
The reason for this is that await context.Response.WriteAsync(jsonObject, Encoding.UTF8); isn't writing to the original body stream it's writing to the memory stream that is seekable. So after you write to it you have to copy it to the original stream. So I believe the code should look like this:
catch (Exception ex)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var jsonObject = JsonConvert.SerializeObject(My Custom Model);
await context.Response.WriteAsync(jsonObject, Encoding.UTF8);
context.Response.Body.Seek(0, SeekOrigin.Begin); //IMPORTANT!
await responseBody.CopyToAsync(originalBodyStream); //IMPORTANT!
return;
}
There is a wonderful article explaining in detail your problem - Using Middleware to trap Exceptions in Asp.Net Core.
What you need to remember about middleware is the following:
Middleware is added to your app during Startup, as you saw above. The order in which you call the Use... methods does matter! Middleware is "waterfalled" down through until either all have been executed, or one stops execution.
The first things passed to your middleware is a request delegate. This is a delegate that takes the current HttpContext object and executes it. Your middleware saves this off upon creation, and uses it in the Invoke() step.
Invoke() is where the work is done. Whatever you want to do to the request/response as part of your middleware is done here. Some other usages for middleware might be to authorize a request based on a header or inject a header in to the request or response
So what you do, you write a new exception type, and a middleware handler to trap your exception:
New Exception type class:
public class HttpStatusCodeException : Exception
{
public int StatusCode { get; set; }
public string ContentType { get; set; } = #"text/plain";
public HttpStatusCodeException(int statusCode)
{
this.StatusCode = statusCode;
}
public HttpStatusCodeException(int statusCode, string message) : base(message)
{
this.StatusCode = statusCode;
}
public HttpStatusCodeException(int statusCode, Exception inner) : this(statusCode, inner.ToString()) { }
public HttpStatusCodeException(int statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())
{
this.ContentType = #"application/json";
}
}
And the middlware handler:
public class HttpStatusCodeExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<HttpStatusCodeExceptionMiddleware> _logger;
public HttpStatusCodeExceptionMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
_next = next ?? throw new ArgumentNullException(nameof(next));
_logger = loggerFactory?.CreateLogger<HttpStatusCodeExceptionMiddleware>() ?? throw new ArgumentNullException(nameof(loggerFactory));
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (HttpStatusCodeException ex)
{
if (context.Response.HasStarted)
{
_logger.LogWarning("The response has already started, the http status code middleware will not be executed.");
throw;
}
context.Response.Clear();
context.Response.StatusCode = ex.StatusCode;
context.Response.ContentType = ex.ContentType;
await context.Response.WriteAsync(ex.Message);
return;
}
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class HttpStatusCodeExceptionMiddlewareExtensions
{
public static IApplicationBuilder UseHttpStatusCodeExceptionMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<HttpStatusCodeExceptionMiddleware>();
}
}
Then use your new middleware:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseHttpStatusCodeExceptionMiddleware();
}
else
{
app.UseHttpStatusCodeExceptionMiddleware();
app.UseExceptionHandler();
}
app.UseStaticFiles();
app.UseMvc();
}
The end use is simple:
throw new HttpStatusCodeException(StatusCodes.Status400BadRequest, #"You sent bad stuff");
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