Skip to content

Commit 41eebde

Browse files
authored
Use [DebuggerDisableUserUnhandledExceptions] (#57148)
1 parent e31445e commit 41eebde

File tree

4 files changed

+37
-2
lines changed

4 files changed

+37
-2
lines changed

src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public Task Render(HttpContext context)
3333
return _renderer.Dispatcher.InvokeAsync(() => RenderComponentCore(context));
3434
}
3535

36+
// We do not want the debugger to consider NavigationExceptions caught by this method as user-unhandled.
37+
[DebuggerDisableUserUnhandledExceptions]
3638
private async Task RenderComponentCore(HttpContext context)
3739
{
3840
context.Response.ContentType = RazorComponentResultExecutor.DefaultContentType;

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using System.Diagnostics.CodeAnalysis;
56
using System.Text.Encodings.Web;
67
using Microsoft.AspNetCore.Components.Rendering;
@@ -88,6 +89,8 @@ public ValueTask<IHtmlAsyncContent> PrerenderComponentAsync(
8889
ParameterView parameters)
8990
=> PrerenderComponentAsync(httpContext, componentType, prerenderMode, parameters, waitForQuiescence: true);
9091

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

135+
// We do not want the debugger to consider NavigationExceptions caught by this method as user-unhandled.
136+
[DebuggerDisableUserUnhandledExceptions]
132137
internal async ValueTask<PrerenderedComponentHtmlContent> RenderEndpointComponent(
133138
HttpContext httpContext,
134139
[DynamicallyAccessedMembers(Component)] Type rootComponentType,
@@ -198,7 +203,8 @@ public static ValueTask<PrerenderedComponentHtmlContent> HandleNavigationExcepti
198203
throw new InvalidOperationException(
199204
"A navigation command was attempted during prerendering after the server already started sending the response. " +
200205
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
201-
"response and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.");
206+
"response and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.",
207+
navigationException);
202208
}
203209
else if (IsPossibleExternalDestination(httpContext.Request, navigationException.Location)
204210
&& IsProgressivelyEnhancedNavigation(httpContext.Request))

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics;
45
using System.Runtime.InteropServices;
56
using System.Text;
67
using System.Text.Encodings.Web;
@@ -37,6 +38,8 @@ public void InitializeStreamingRenderingFraming(HttpContext httpContext, bool is
3738
}
3839
}
3940

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

6770
// At this point we yield the sync context. SSR batches may then be emitted at any time.
68-
await writer.FlushAsync();
71+
await writer.FlushAsync();
6972
await untilTaskCompleted;
7073
}
7174
catch (NavigationException navigationException)
@@ -74,6 +77,10 @@ public async Task SendStreamingUpdatesAsync(HttpContext httpContext, Task untilT
7477
}
7578
catch (Exception ex)
7679
{
80+
// Rethrowing also informs the debugger that this exception should be considered user-unhandled unlike NavigationExceptions,
81+
// but calling BreakForUserUnhandledException here allows the debugger to break before we modify the HttpContext.
82+
Debugger.BreakForUserUnhandledException(ex);
83+
7784
// Theoretically it might be possible to let the error middleware run, capture the output,
7885
// then emit it in a special format so the JS code can display the error page. However
7986
// for now we're not going to support that and will simply emit a message.

src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddlewareImpl.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,13 @@ private static void SetExceptionHandlerFeatures(ErrorContext errorContext)
102102
/// </summary>
103103
/// <param name="context"></param>
104104
/// <returns></returns>
105+
[DebuggerDisableUserUnhandledExceptions]
105106
public async Task Invoke(HttpContext context)
106107
{
108+
// We want to avoid treating exceptions as user-unhandled if an exception filter like the DatabaseDeveloperPageExceptionFilter
109+
// handles the exception rather than letting it flow to the default DisplayException method. This is because we don't want to stop the
110+
// debugger if the developer shouldn't be handling the exception and instead just needs to do something like click a link to run a
111+
// database migration.
107112
try
108113
{
109114
await _next(context);
@@ -122,6 +127,11 @@ public async Task Invoke(HttpContext context)
122127
context.Response.StatusCode = StatusCodes.Status499ClientClosedRequest;
123128
}
124129

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

@@ -131,6 +141,8 @@ public async Task Invoke(HttpContext context)
131141
{
132142
_logger.ResponseStartedErrorPageMiddleware();
133143
_metrics.RequestException(exceptionName, ExceptionResult.Skipped, handler: null);
144+
145+
// Rethrowing informs the debugger that this exception should be considered user-unhandled.
134146
throw;
135147
}
136148

@@ -161,11 +173,16 @@ public async Task Invoke(HttpContext context)
161173
}
162174
catch (Exception ex2)
163175
{
176+
// It might make sense to call BreakForUserUnhandledException for ex2 after we do the same for the original exception.
177+
// But for now, considering the rarity of user-defined IDeveloperPageExceptionFilters, we're not for simplicity.
178+
164179
// If there's a Exception while generating the error page, re-throw the original exception.
165180
_logger.DisplayErrorPageException(ex2);
166181
}
167182

168183
_metrics.RequestException(exceptionName, ExceptionResult.Unhandled, handler: null);
184+
185+
// Rethrowing informs the debugger that this exception should be considered user-unhandled.
169186
throw;
170187
}
171188

@@ -178,6 +195,9 @@ public async Task Invoke(HttpContext context)
178195
// Assumes the response headers have not been sent. If they have, still attempt to write to the body.
179196
private Task DisplayException(ErrorContext errorContext)
180197
{
198+
// We need to inform the debugger that this exception should be considered user-unhandled since it wasn't fully handled by an exception filter.
199+
Debugger.BreakForUserUnhandledException(errorContext.Exception);
200+
181201
var httpContext = errorContext.HttpContext;
182202
var headers = httpContext.Request.GetTypedHeaders();
183203
var acceptHeader = headers.Accept;

0 commit comments

Comments
 (0)