@@ -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