|
// 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.Diagnostics.Metrics;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.RazorViews;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.StackTrace.Sources;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Diagnostics;
/// <summary>
/// Captures synchronous and asynchronous exceptions from the pipeline and generates error responses.
/// </summary>
internal class DeveloperExceptionPageMiddlewareImpl
{
private readonly RequestDelegate _next;
private readonly DeveloperExceptionPageOptions _options;
private readonly ILogger _logger;
private readonly IFileProvider _fileProvider;
private readonly DiagnosticSource _diagnosticSource;
private readonly DiagnosticsMetrics _metrics;
private readonly ExceptionDetailsProvider _exceptionDetailsProvider;
private readonly Func<ErrorContext, Task> _exceptionHandler;
private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html");
private readonly ExtensionsExceptionJsonContext _serializationContext;
private readonly IProblemDetailsService? _problemDetailsService;
public DeveloperExceptionPageMiddlewareImpl(
RequestDelegate next,
IOptions<DeveloperExceptionPageOptions> options,
ILoggerFactory loggerFactory,
IWebHostEnvironment hostingEnvironment,
DiagnosticSource diagnosticSource,
IEnumerable<IDeveloperPageExceptionFilter> filters,
IMeterFactory meterFactory,
IOptions<JsonOptions>? jsonOptions = null,
IProblemDetailsService? problemDetailsService = null)
{
ArgumentNullException.ThrowIfNull(next);
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(filters);
_next = next;
_options = options.Value;
_logger = loggerFactory.CreateLogger<DeveloperExceptionPageMiddleware>();
_fileProvider = _options.FileProvider ?? hostingEnvironment.ContentRootFileProvider;
_diagnosticSource = diagnosticSource;
_metrics = new DiagnosticsMetrics(meterFactory);
_exceptionDetailsProvider = new ExceptionDetailsProvider(_fileProvider, _logger, _options.SourceCodeLineCount);
_exceptionHandler = DisplayException;
_serializationContext = CreateSerializationContext(jsonOptions?.Value);
_problemDetailsService = problemDetailsService;
foreach (var filter in Enumerable.Reverse(filters))
{
var nextFilter = _exceptionHandler;
_exceptionHandler = errorContext => filter.HandleExceptionAsync(errorContext, nextFilter);
}
}
private static ExtensionsExceptionJsonContext CreateSerializationContext(JsonOptions? jsonOptions)
{
// Create context from configured options to get settings such as PropertyNamePolicy and DictionaryKeyPolicy.
jsonOptions ??= new JsonOptions();
return new ExtensionsExceptionJsonContext(new JsonSerializerOptions(jsonOptions.SerializerOptions));
}
private static void SetExceptionHandlerFeatures(ErrorContext errorContext)
{
var httpContext = errorContext.HttpContext;
var exceptionHandlerFeature = new ExceptionHandlerFeature()
{
Error = errorContext.Exception,
Path = httpContext.Request.Path.ToString(),
Endpoint = httpContext.GetEndpoint(),
RouteValues = httpContext.Features.Get<IRouteValuesFeature>()?.RouteValues
};
httpContext.Features.Set<IExceptionHandlerFeature>(exceptionHandlerFeature);
httpContext.Features.Set<IExceptionHandlerPathFeature>(exceptionHandlerFeature);
}
/// <summary>
/// Process an individual request.
/// </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);
}
catch (Exception ex)
{
var exceptionName = ex.GetType().FullName!;
if ((ex is OperationCanceledException || ex is IOException) && context.RequestAborted.IsCancellationRequested)
{
_logger.RequestAbortedException();
_metrics.RequestException(exceptionName, ExceptionResult.Aborted, handler: null);
if (!context.Response.HasStarted)
{
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;
}
DiagnosticsTelemetry.ReportUnhandledException(_logger, context, ex);
if (context.Response.HasStarted)
{
_logger.ResponseStartedErrorPageMiddleware();
_metrics.RequestException(exceptionName, ExceptionResult.Skipped, handler: null);
// Rethrowing informs the debugger that this exception should be considered user-unhandled.
throw;
}
try
{
context.Response.Clear();
// Preserve the status code that would have been written by the server automatically when a BadHttpRequestException is thrown.
if (ex is BadHttpRequestException badHttpRequestException)
{
context.Response.StatusCode = badHttpRequestException.StatusCode;
}
else
{
context.Response.StatusCode = 500;
}
await _exceptionHandler(new ErrorContext(context, ex));
const string eventName = "Microsoft.AspNetCore.Diagnostics.UnhandledException";
if (_diagnosticSource.IsEnabled(eventName))
{
WriteDiagnosticEvent(_diagnosticSource, eventName, new { httpContext = context, exception = ex });
}
_metrics.RequestException(exceptionName, ExceptionResult.Unhandled, handler: null);
return;
}
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;
}
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026",
Justification = "The values being passed into Write have the commonly used properties being preserved with DynamicDependency.")]
static void WriteDiagnosticEvent<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TValue>(DiagnosticSource diagnosticSource, string name, TValue value)
=> diagnosticSource.Write(name, value);
}
// 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;
// If the client does not ask for HTML just format the exception as plain text
if (acceptHeader == null || !acceptHeader.Any(h => h.IsSubsetOf(_textHtmlMediaType)))
{
return DisplayExceptionContent(errorContext);
}
if (errorContext.Exception is ICompilationException compilationException)
{
return DisplayCompilationException(httpContext, compilationException);
}
return DisplayRuntimeException(httpContext, errorContext.Exception);
}
private async Task DisplayExceptionContent(ErrorContext errorContext)
{
var httpContext = errorContext.HttpContext;
if (_problemDetailsService is not null)
{
SetExceptionHandlerFeatures(errorContext);
}
if (_problemDetailsService == null || !await _problemDetailsService.TryWriteAsync(new()
{
HttpContext = httpContext,
ProblemDetails = CreateProblemDetails(errorContext, httpContext),
Exception = errorContext.Exception
}))
{
httpContext.Response.ContentType = "text/plain; charset=utf-8";
var sb = new StringBuilder();
sb.AppendLine(errorContext.Exception.ToString());
sb.AppendLine();
sb.AppendLine("HEADERS");
sb.AppendLine("=======");
foreach (var pair in httpContext.Request.Headers)
{
sb.AppendLine(FormattableString.Invariant($"{pair.Key}: {pair.Value}"));
}
await httpContext.Response.WriteAsync(sb.ToString());
}
}
private ProblemDetails CreateProblemDetails(ErrorContext errorContext, HttpContext httpContext)
{
var problemDetails = new ProblemDetails
{
Title = TypeNameHelper.GetTypeDisplayName(errorContext.Exception.GetType()),
Detail = errorContext.Exception.Message,
Status = httpContext.Response.StatusCode
};
// Problem details source gen serialization doesn't know about IHeaderDictionary or RouteValueDictionary.
// Serialize payload to a JsonElement here. Problem details serialization can write JsonElement in extensions dictionary.
problemDetails.Extensions["exception"] = JsonSerializer.SerializeToElement(new ExceptionExtensionData
(
details: errorContext.Exception.ToString(),
headers: httpContext.Request.Headers,
path: httpContext.Request.Path.ToString(),
endpoint: httpContext.GetEndpoint()?.ToString(),
routeValues: httpContext.Features.Get<IRouteValuesFeature>()?.RouteValues
), _serializationContext.ExceptionExtensionData);
return problemDetails;
}
private Task DisplayCompilationException(
HttpContext context,
ICompilationException compilationException)
{
var model = new CompilationErrorPageModel(_options);
var errorPage = new CompilationErrorPage(model);
if (compilationException.CompilationFailures == null)
{
return errorPage.ExecuteAsync(context);
}
foreach (var compilationFailure in compilationException.CompilationFailures)
{
if (compilationFailure == null)
{
continue;
}
var stackFrames = new List<StackFrameSourceCodeInfo>();
var exceptionDetails = new ExceptionDetails(compilationFailure.FailureSummary!, stackFrames);
model.ErrorDetails.Add(exceptionDetails);
model.CompiledContent.Add(compilationFailure.CompiledContent);
if (compilationFailure.Messages == null)
{
continue;
}
var sourceLines = compilationFailure
.SourceFileContent?
.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
foreach (var item in compilationFailure.Messages)
{
if (item == null)
{
continue;
}
var frame = new StackFrameSourceCodeInfo
{
File = compilationFailure.SourceFilePath,
Line = item.StartLine,
Function = string.Empty
};
if (sourceLines != null)
{
_exceptionDetailsProvider.ReadFrameContent(frame, sourceLines, item.StartLine, item.EndLine);
}
frame.ErrorDetails = item.Message;
stackFrames.Add(frame);
}
}
return errorPage.ExecuteAsync(context);
}
private Task DisplayRuntimeException(HttpContext context, Exception ex)
{
var endpoint = context.GetEndpoint();
EndpointModel? endpointModel = null;
if (endpoint != null)
{
endpointModel = new EndpointModel
{
DisplayName = endpoint.DisplayName,
Metadata = endpoint.Metadata
};
if (endpoint is RouteEndpoint routeEndpoint)
{
endpointModel.RoutePattern = routeEndpoint.RoutePattern.RawText;
endpointModel.Order = routeEndpoint.Order;
var httpMethods = endpoint.Metadata.GetMetadata<IHttpMethodMetadata>()?.HttpMethods;
if (httpMethods != null)
{
endpointModel.HttpMethods = string.Join(", ", httpMethods);
}
}
}
var request = context.Request;
var title = Resources.ErrorPageHtml_Title;
if (ex is BadHttpRequestException badHttpRequestException)
{
var badRequestReasonPhrase = WebUtilities.ReasonPhrases.GetReasonPhrase(badHttpRequestException.StatusCode);
if (!string.IsNullOrEmpty(badRequestReasonPhrase))
{
title = badRequestReasonPhrase;
}
}
var model = new ErrorPageModel
{
Options = _options,
ErrorDetails = _exceptionDetailsProvider.GetDetails(ex),
Query = request.Query,
Cookies = request.Cookies,
Headers = request.Headers,
RouteValues = request.RouteValues,
Endpoint = endpointModel,
Title = title,
};
var errorPage = new ErrorPage(model);
return errorPage.ExecuteAsync(context);
}
}
internal sealed class ExceptionExtensionData
{
public ExceptionExtensionData(string details, IHeaderDictionary headers, string path, string? endpoint, RouteValueDictionary? routeValues)
{
Details = details;
Headers = headers;
Path = path;
Endpoint = endpoint;
RouteValues = routeValues;
}
public string Details { get; }
public IHeaderDictionary Headers { get; }
public string Path { get; }
public string? Endpoint { get; }
public RouteValueDictionary? RouteValues { get; }
}
[JsonSerializable(typeof(ExceptionExtensionData))]
internal sealed partial class ExtensionsExceptionJsonContext : JsonSerializerContext
{
}
|