-
Notifications
You must be signed in to change notification settings - Fork 10.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
IExceptionHandler for exception handler middleware #46280
Comments
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
|
@Tratcher Would you accept a PR for this one when the API got approved? |
Likely. Thanks for offering. |
Do we need to pass |
API Review Notes:
API Approved as proposed! Assembly: Microsoft.AspNetCore.Diagnostics.Abstractions
Microsoft.Extensions.DependencyInjection;
public static class ExceptionHandlerServiceCollectionExtensions
{
// Adds a singleton IExceptionHandler service. You can pull request services from HttpContext.
+ public static IServiceCollection AddExceptionHandler<T>(this IServiceCollection services) where T is IExceptionHandler;
}
namespace Microsoft.AspNetCore.Diagnostics;
+ public interface IExceptionHandler
+ {
+ ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken);
+ } |
Thanks for contacting us. We're moving this issue to the |
I'd like a more generic mechanism that lets me completely replace the default exception handler. I have my own thing (registered with
I'd like a bit more clarification on exactly what you mean by "normal control flow". I've read long debates over this and if anything, what won out in terms of official advice from Microsoft was to use exceptions relatively liberally; for an unexpected network disconnection or something, yes, but also for broken user input. I throw an exception if an invalid value is passed into my REST API for example. Is this wrong? Why? It's a one-off thing and bad input isn't generally expected. It works very nicely to curtail any further execution, which is impossible if the input is broken and cannot be used. |
How is that different from using your own middleware? We've avoided making log levels configurable anywhere in the platform because it doesn't scale up or down. At one extreme you'd need an option for every log statement. At the other you'd need an option for every component which isn't significantly different from configuring the levels via appsettings.json. Log filters are another way of tuning your log outputs.
The guidance is nearly the opposite, avoid exceptions for any expected scenario. E.g. when dealing with user input use APIs like TryParse and provide the user with a direct comment about what input is expected. Types like Socket and Stream have been around forever and don't provide better alternatives, but even there we try to handle those exceptions as early as possible in the caller rather than flowing them through the stack. Why? Exceptions are very expensive, and they're also hard to reason about when thrown from a deeply nested component, you lose too much context to provide a reasonable mitigation. The ExceptionHandlerMiddleware is a fallback of last resort, it can't provide granular, meaningful handling of exceptions from anywhere in your app, only generic handling like "Oops, something went wrong, please try again later.". |
I don't quite understand what you mean - use my own middleware in what way, for what? For handling exceptions? I do, but the default handler still spits out logs that have to be suppressed with an unintuative log level configuration that reads like "don't log exceptions". It feels hacky.
It's not really configuring log levels, it's more "don't even register the default exception handler because here's my one that's gonna replace it".
Again, I spent quite some time thinking about this, and reading up on it, and opinion is seriously divided. I didn't find any "clear guidance" anywhere on precisely how to use exceptions. But say I have some code where I'm creating a game from a REST API controller. It calls If the official guidance is not to use exceptions like this, I think it's seriously debatable. I think the exception mechanism allowing this control flow is way too useful to be "last resort" only and if they're so heavyweight, well, .NET should introduce some kind of lightweight exception. The ExceptionHandlerMiddleware can't provide granular, meaningful handling of exceptions precisely because it's totally generic. I know what my exception types and their contents mean. That's why I want to replace the default handler and handle my exceptions myself. |
It's only registered in the template. If you don't want it you can remove this line: aspnetcore/src/ProjectTemplates/Web.ProjectTemplates/content/StarterWeb-CSharp/Program.cs Line 125 in 6796d5b
Here's the guidance I was referring to: https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/exception-throwing
|
Odd, I have a .NET 6 web project and it's getting registered anyway, and I don't have that line. Are you sure that's not just for using a specific error page? I mean, if you don't have that line, there's still some kind of default exception handling middleware isn't there?
Yeah, like I said, code littered with |
If you don't have that line then the only default is the server catching exceptions thrown out of the middleware pipeline, logging, and returning an empty 500. If you handle the exception in your own middleware and don't re-throw it, neither of these components will log it. What's the log look like? |
Here's an example:
My middleware is running via |
Oh, you don't have a custom middleware the, you've just plugged into UseExceptionHandler's callback. The middleware will log before invoking your callback. Replacing the middleware is pretty simple, it's just a try/catch around the |
Don't quite understand you, I have:
Where is the |
UseExceptionHandler needs to be replaced completely to get the control you want, but it doesn't take much: app.Use(async (context, next) =>
{
try
{
await next(context);
}
catch (Exception ex)
{
if (context.Response.HasStarted)
{
// Too late to change anything
throw;
}
/* handle exception */
}
}); |
Also @Tratcher I just noticed your risks comment, "We don't want to encourage developers to use exceptions as normal control flow, it's extremely in-efficient and poor design." In practice, I'm really not sure about this advice. I've read that you have to be throwing 100 exceptions per second to start having a problem. How often is that realistically going to happen, especially for an API that isn't public or heavily-used? And to lose the exception mechanism for a generic error handler in ASP.NET, avoiding the need for a bunch of defensive coding around everything, seems a shame. Please read my issue #47020 for further comments on this. |
Exceptions are extremely expensive, even more expensive than usual when they are in the ASP.NET Core pipeline which is fully asynchronous and nested (we're looking at ways to make this cheaper but it's still very expensive). In fact, there are teams with high scale services that see performance problems with exceptions and are trying to avoid them happening. There's no way we'd do #47020. |
Is there no way to have a mechanism like exceptions that isn't so expensive? It's just way more convenient to be able to throw and handle the error at the right level (often my centralized exception handling middleware) than pass some error info back up the chain. |
It isn't something we'd ever recommend; I think it's fine for your custom middleware to continue to do this. |
I find that really confusing guidance. |
@Tratcher can you file some follow up issues around how we plan to use this with problem details. |
Background and Motivation
The exception handling middleware is designed to take unexpected, unhandled exceptions, log them, and render a customizable response for the client. This is a last ditch effort to produce something human readable, otherwise the response will receive an empty 500. This is not intended for known cases or control flow. Expected exceptions should be handled at the individual call sites.
Ask: Some exceptions like database exceptions could happen in many places and having to handle those in each place requires a lot of redundant code. How can handling of known exceptions be centralized?
Proposed API
The IExceptionHandler is a new interface that can have multiple instances registered with DI. The existing exception handling middleware will resolve these and loop through them until something matches. If none match, a higher level log (warning?) will be written. Exception handlers are not run if the response has already started. The existing IDeveloperPageExceptionFilter is used similarly with the developer exception page.
Usage Examples
Risks
We don't want to encourage developers to use exceptions as normal control flow, it's extremely in-efficient and poor design. We should document that expectation and avoid using this capability in our own components.
This should not be confused with the MVC IExceptionFilter.
https://learn.microsoft.com/en-us/dotnet/api/system.web.mvc.iexceptionfilter
The text was updated successfully, but these errors were encountered: