Production (and Debug) replacement for app.UseDeveloperExceptionPage()
.
Exception handler middleware in ASP.NET (API solutions mainly) to get exception as JSON object with rfc7807 standard proposal in mind.
Implementing provided abstract class with simplistic your own middleware gives ability to handle specific exceptions and control retuerned state codes (400+; 500+) with Json data payload, describing error situation and throw exception(s).
Cosider "star" this project and/or better
See also other packages for some other/related functionality in Asp.Net Core (mostly APIs):
- API dynamic FrontPage (with binaries versioning approaches)
- Health check with JSON result + Health page
- Configuration validation
Package includes most basic implementation of abstract class, ready to use right away, which can be wired up by adding app.AddJsonExceptionHandler();
into program.cs
(or startup.cs
if you use older approach).
This will return state code 500 with Json object.
More advanced way is to add your own middleware based on provided abstract base class as in this example (example mimics included default middleware):
/// <summary>
/// Own middleware with provided base middleware class.
/// </summary>
public class ApiJsonErrorMiddleware : ApiJsonExceptionMiddleware
{
// use either this simplified constructor
public ApiJsonErrorMiddleware(RequestDelegate next, ILogger<ApiJsonExceptionMiddleware> logger, bool showStackTrace)
: base(next, logger, showStackTrace)
{
}
// or use this constructor to supply extended options
public ApiJsonErrorMiddleware(RequestDelegate next, ILogger<ApiJsonExceptionMiddleware> logger, ApiJsonExceptionOptions options)
: base(next, logger, options)
{
}
}
After it is created, you can register it in API Program.cs
(or Startup.cs
Configure
method) like this (somewhere in the very beginning setup for app
):
// When used constructor with options and relaying on default settings:
app.AddJsonExceptionHandler<ApiJsonErrorMiddleware>();
// When used constructor with boolean:
app.AddJsonExceptionHandler<ApiJsonErrorMiddleware>(true);
// When used with options setting:
app.AddJsonExceptionHandler<ApiJsonErrorMiddleware>(new ApiJsonExceptionOptions { OmitSources = new HashSet<string> { "SomeMiddleware" }, ShowStackTrace = true });
The only parameter in simple constructor controls whether StackTrace is shown to consumer.
In example above we can control it by environment variable so it is shown during API development, but hidden in any other environment. If you put constant true/false in stead - it is either shown always or hidden always.
For options - you can set the same showStackTrace
boolean and also specify list of stack trace frames to be filtered out from being shown. It is OmitSources
property, containing list (HashSet) of strings, which should not be a part of file path in stack trace frame.
For example, if you set it to new HashSet<string> { "middleware" }
, it will filter out all middleware components (given they have string "middleware" in their file name or in path).
If you want to handle (return data on) some specific exceptions, then you should override HandleSpecialException
method from base class. There you can check whether exception is of this special type and modify returned Json data structure accordingly:
/// <summary>
/// This method is called from base class handler to add more information to Json Error object.
/// Here all special exception types should be handled, so API Json Error returns appropriate data.
/// </summary>
/// <param name="apiError">ApiError object, which gets returned from API in case of exception/error. Provided by </param>
/// <param name="exception">Exception which got bubbled up from somewhere deep in API logic.</param>
protected override ApiError HandleSpecialException(ApiError apiError, Exception exception)
{
// When using FluentValidation, could use also handler for its ValidationException in stead of this custom one
if (exception is SampleDataValidationException validationException)
{
apiError.Status = 400; // or 422
apiError.ErrorType = ApiErrorType.DataValidationError;
apiError.ValidationErrors
.AddRange(
validationException.ValidationErrors.Select(failure =>
new ApiDataValidationError
{
Message = failure.ValidationMessage,
PropertyName = failure.PropertyName,
AttemptedValue = failure.AppliedValue
}));
// This does not log error (e.g. Not show up in ApplicationInsights), but still returns Json error.
apiError.ErrorBehavior = ApiErrorBehavior.RespondWithError;
}
if (exception is AccessViolationException securityException)
{
apiError.Status = 401; // or 403
apiError.ErrorType = ApiErrorType.AccessRestrictedError;
}
if (exception is SampleDatabaseException dbException)
{
apiError.Status = 500;
if (dbException.ErrorType == DatabaseProblemType.WrongSyntax)
{
apiError.ErrorType = ApiErrorType.StorageError;
}
}
if (exception is NotImplementedException noImplemented)
{
apiError.Status = 501;
apiError.Title = "Functionality is not yet implemented.";
}
if (exception is OperationCanceledException operationCanceledException)
{
// This returns empty (200) response and does not log error.
apiError.ErrorBehavior = ApiErrorBehavior.Ignore;
}
return apiError;
}
In case of data validation exceptions, when they are handled fully (as shown in example above), Json property validationErrors
is provided:
{
"type": "DataValidationError",
"title": "There are validation errors.",
"status": 400,
"requestedUrl": "/api/sample/validation",
"errorType": 3,
"exceptionType": "SampleDataValidationException",
"innerException": {
"title": "Some inner exception",
"exceptionType": "ArgumentNullException",
"innerException": {
"title": "Deepest inner exception",
"exceptionType": "NotImplementedException",
"innerException": null
}
},
"stackTrace": [
"at ValidationError() in Sample.AspNet5.Logic\\SampleLogic.cs: line 50",
"at ThrowValidationException() in Sample.AspNet5.Api\\Services\\HomeController.cs: line 117",
"at Invoke(HttpContext httpContext) in Source\\Salix.ExceptionHandling\\ApiJsonExceptionMiddleware.cs: line 56"
],
"validationErrors": [
{
"propertyName": "Name",
"attemptedValue": "",
"message": "Missing/Empty"
},
{
"propertyName": "Id",
"attemptedValue": null,
"message": "Cannot be null"
},
{
"propertyName": "Description",
"attemptedValue": "Lorem Ipsum very long...",
"message": "Text is too long"
},
{
"propertyName": "Birthday",
"attemptedValue": "2054-06-22T23:55:26.1708087+03:00",
"message": "Cannot be in future"
}
]
}
By default Json error handler will write exception to configured ILogger
instance (you control where and how it writes - AppInsights, File, Debug, Console etc.)
and also creates Json error response and returns it to caller with specified HttpStatus code (400+, 500+).
If you use custom exception handler method, you can intercept specific exceptions and make error handler do not write an error statement to ILogger
and/or return Json error object at all (returns 200 status code with empty response).
To control it, in specific exception handling method, intercept your special exception and set ApiError
object property ErrorBehavior
to desired behaviour.
if (exception is OperationCanceledException operationCanceledException)
{
// This returns empty (200) response and does not log error.
apiError.ErrorBehavior = ApiErrorBehavior.Ignore;
}
if (exception is TaskCanceledException taskCanceledException)
{
// This does not log error, but still returns Json error.
apiError.ErrorBehavior = ApiErrorBehavior.RespondWithError;
apiError.Status = (int)HttpStatusCode.UnprocessableEntity; // or other by your design
apiError.ErrorType = ApiErrorType.CancelledOperation;
}
It could come handy to ignore user cancelled operations when using async code with CancellationToken.
Since .Net 8.0 there is additional way to handle global exceptions in ASP.NET framework by implementing one or more IExceptionHandler implementations. See MS Docs.
This package provides implementation for this approach and it can be used in this way:
Create class, deriving from ApiJsonExceptionHandler
(which implements IExceptionHandler interface in it)
internal sealed class GlobalExceptionHandler : ApiJsonExceptionHandler
{
public GlobalExceptionHandler(ILogger<ApiJsonExceptionHandler> logger)
: base(logger, new ApiJsonExceptionOptions { ShowStackTrace = true })
{
}
// If you want to handle some exceptions more precise...
protected override ApiError HandleSpecialException(ApiError apiError, Exception exception)
{
if (exception is AccessViolationException securityException)
{
apiError.Status = 401; // or 403
apiError.ErrorType = ApiErrorType.AccessRestrictedError;
}
if (exception is SampleDatabaseException dbException)
{
apiError.Status = 500;
if (dbException.ErrorType == DatabaseProblemType.WrongSyntax)
{
apiError.ErrorType = ApiErrorType.StorageError;
}
}
}
}
Then register this class with dependency injection container in program.cs
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
and register middleware:
app.UseExceptionHandler(_ => { });
Options and Error behaviour is to be handled the same way as described above for Error handling middleware.