Skip to content

Use [DebuggerDisableUserUnhandledExceptions] #57148

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

Merged
merged 4 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public Task Render(HttpContext context)
return _renderer.Dispatcher.InvokeAsync(() => RenderComponentCore(context));
}

// We do not want the debugger to consider NavigationExceptions caught by this method as user-unhandled.
[DebuggerDisableUserUnhandledExceptions]
private async Task RenderComponentCore(HttpContext context)
{
context.Response.ContentType = RazorComponentResultExecutor.DefaultContentType;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Components.Rendering;
Expand Down Expand Up @@ -88,6 +89,8 @@ public ValueTask<IHtmlAsyncContent> PrerenderComponentAsync(
ParameterView parameters)
=> PrerenderComponentAsync(httpContext, componentType, prerenderMode, parameters, waitForQuiescence: true);

// We do not want the debugger to consider NavigationExceptions caught by this method as user-unhandled.
[DebuggerDisableUserUnhandledExceptions]
public async ValueTask<IHtmlAsyncContent> PrerenderComponentAsync(
HttpContext httpContext,
[DynamicallyAccessedMembers(Component)] Type componentType,
Expand Down Expand Up @@ -129,6 +132,8 @@ public async ValueTask<IHtmlAsyncContent> PrerenderComponentAsync(
}
}

// We do not want the debugger to consider NavigationExceptions caught by this method as user-unhandled.
[DebuggerDisableUserUnhandledExceptions]
internal async ValueTask<PrerenderedComponentHtmlContent> RenderEndpointComponent(
HttpContext httpContext,
[DynamicallyAccessedMembers(Component)] Type rootComponentType,
Expand Down Expand Up @@ -198,7 +203,8 @@ public static ValueTask<PrerenderedComponentHtmlContent> HandleNavigationExcepti
throw new InvalidOperationException(
"A navigation command was attempted during prerendering after the server already started sending the response. " +
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
"response and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.");
"response and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.",
navigationException);
}
else if (IsPossibleExternalDestination(httpContext.Request, navigationException.Location)
&& IsProgressivelyEnhancedNavigation(httpContext.Request))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Encodings.Web;
Expand Down Expand Up @@ -37,6 +38,8 @@ public void InitializeStreamingRenderingFraming(HttpContext httpContext, bool is
}
}

// We do not want the debugger to consider NavigationExceptions caught by this method as user-unhandled.
[DebuggerDisableUserUnhandledExceptions]
public async Task SendStreamingUpdatesAsync(HttpContext httpContext, Task untilTaskCompleted, TextWriter writer)
{
// Important: do not introduce any 'await' statements in this method above the point where we write
Expand Down Expand Up @@ -65,7 +68,7 @@ public async Task SendStreamingUpdatesAsync(HttpContext httpContext, Task untilT
EmitInitializersIfNecessary(httpContext, writer);

// At this point we yield the sync context. SSR batches may then be emitted at any time.
await writer.FlushAsync();
await writer.FlushAsync();
await untilTaskCompleted;
}
catch (NavigationException navigationException)
Expand All @@ -74,6 +77,10 @@ public async Task SendStreamingUpdatesAsync(HttpContext httpContext, Task untilT
}
catch (Exception ex)
{
// Rethrowing also informs the debugger that this exception should be considered user-unhandled unlike NavigationExceptions,
// but calling BreakForUserUnhandledException here allows the debugger to break before we modify the HttpContext.
Debugger.BreakForUserUnhandledException(ex);

// Theoretically it might be possible to let the error middleware run, capture the output,
// then emit it in a special format so the JS code can display the error page. However
// for now we're not going to support that and will simply emit a message.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,13 @@ private static void SetExceptionHandlerFeatures(ErrorContext errorContext)
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
[DebuggerDisableUserUnhandledExceptions]
public async Task Invoke(HttpContext context)
{
// We want to avoid treating exceptions as user-unhandled if an exception filter like the DatabaseDeveloperPageExceptionFilter
// handles the exception rather than letting it flow to the default DisplayException method. This is because we don't want to stop the
// debugger if the developer shouldn't be handling the exception and instead just needs to do something like click a link to run a
// database migration.
try
{
await _next(context);
Expand All @@ -122,6 +127,11 @@ public async Task Invoke(HttpContext context)
context.Response.StatusCode = StatusCodes.Status499ClientClosedRequest;
}

// Generally speaking, we do not expect application code to handle things like IOExceptions during a request
// body read due to a client disconnect. But aborted requests should be rare in development, and developers
// might be surprised if an IOException propagating through their code was not considered user-unhandled.
// That said, if developers complain, we consider removing the following line.
Debugger.BreakForUserUnhandledException(ex);
return;
}

Expand All @@ -131,6 +141,8 @@ public async Task Invoke(HttpContext context)
{
_logger.ResponseStartedErrorPageMiddleware();
_metrics.RequestException(exceptionName, ExceptionResult.Skipped, handler: null);

// Rethrowing informs the debugger that this exception should be considered user-unhandled.
throw;
}

Expand Down Expand Up @@ -161,11 +173,16 @@ public async Task Invoke(HttpContext context)
}
catch (Exception ex2)
{
// It might make sense to call BreakForUserUnhandledException for ex2 after we do the same for the original exception.
// But for now, considering the rarity of user-defined IDeveloperPageExceptionFilters, we're not for simplicity.

// If there's a Exception while generating the error page, re-throw the original exception.
_logger.DisplayErrorPageException(ex2);
}

_metrics.RequestException(exceptionName, ExceptionResult.Unhandled, handler: null);

// Rethrowing informs the debugger that this exception should be considered user-unhandled.
throw;
}

Expand All @@ -178,6 +195,9 @@ public async Task Invoke(HttpContext context)
// Assumes the response headers have not been sent. If they have, still attempt to write to the body.
private Task DisplayException(ErrorContext errorContext)
{
// We need to inform the debugger that this exception should be considered user-unhandled since it wasn't fully handled by an exception filter.
Debugger.BreakForUserUnhandledException(errorContext.Exception);

var httpContext = errorContext.HttpContext;
var headers = httpContext.Request.GetTypedHeaders();
var acceptHeader = headers.Accept;
Expand Down
Loading