Support plain text exception formatting (#9342)

- If there's no Accept: text/html then print the exception.ToString as plaintext
This commit is contained in:
David Fowler 2019-04-16 18:34:07 -07:00 committed by GitHub
parent 102dd03149
commit 48f0a76ea9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 84 additions and 5 deletions

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.Internal; using Microsoft.AspNetCore.Diagnostics.Internal;
@ -17,11 +18,12 @@ using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.StackTrace.Sources; using Microsoft.Extensions.StackTrace.Sources;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Diagnostics namespace Microsoft.AspNetCore.Diagnostics
{ {
/// <summary> /// <summary>
/// Captures synchronous and asynchronous exceptions from the pipeline and generates HTML error responses. /// Captures synchronous and asynchronous exceptions from the pipeline and generates error responses.
/// </summary> /// </summary>
public class DeveloperExceptionPageMiddleware public class DeveloperExceptionPageMiddleware
{ {
@ -32,6 +34,7 @@ namespace Microsoft.AspNetCore.Diagnostics
private readonly DiagnosticSource _diagnosticSource; private readonly DiagnosticSource _diagnosticSource;
private readonly ExceptionDetailsProvider _exceptionDetailsProvider; private readonly ExceptionDetailsProvider _exceptionDetailsProvider;
private readonly Func<ErrorContext, Task> _exceptionHandler; private readonly Func<ErrorContext, Task> _exceptionHandler;
private static readonly MediaTypeHeaderValue _textPlainMediaType = new MediaTypeHeaderValue("text/html");
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DeveloperExceptionPageMiddleware"/> class /// Initializes a new instance of the <see cref="DeveloperExceptionPageMiddleware"/> class
@ -127,13 +130,34 @@ namespace Microsoft.AspNetCore.Diagnostics
// Assumes the response headers have not been sent. If they have, still attempt to write to the body. // Assumes the response headers have not been sent. If they have, still attempt to write to the body.
private Task DisplayException(ErrorContext errorContext) private Task DisplayException(ErrorContext errorContext)
{ {
var compilationException = errorContext.Exception as ICompilationException; var httpContext = errorContext.HttpContext;
if (compilationException != null) 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(_textPlainMediaType)))
{ {
return DisplayCompilationException(errorContext.HttpContext, compilationException); httpContext.Response.ContentType = "text/plain";
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($"{pair.Key}: {pair.Value}");
}
return httpContext.Response.WriteAsync(sb.ToString());
} }
return DisplayRuntimeException(errorContext.HttpContext, errorContext.Exception); if (errorContext.Exception is ICompilationException compilationException)
{
return DisplayCompilationException(httpContext, compilationException);
}
return DisplayRuntimeException(httpContext, errorContext.Exception);
} }
private Task DisplayCompilationException( private Task DisplayCompilationException(

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Net.Http.Headers;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@ -45,6 +46,58 @@ namespace Microsoft.AspNetCore.Diagnostics
Assert.Null(listener.DiagnosticHandledException?.Exception); Assert.Null(listener.DiagnosticHandledException?.Exception);
} }
[Fact]
public async Task ErrorPageWithAcceptHeaderForHtmlReturnsHtml()
{
// Arrange
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseDeveloperExceptionPage();
app.Run(context =>
{
throw new Exception("Test exception");
});
});
var server = new TestServer(builder);
// Act
var client = server.CreateClient();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));
var response = await client.GetAsync("/path");
// Assert
var responseText = await response.Content.ReadAsStringAsync();
Assert.Equal("text/html", response.Content.Headers.ContentType.MediaType);
Assert.Contains("<html", responseText);
Assert.Contains("Test exception", responseText);
}
[Fact]
public async Task ErrorPageWithoutAcceptHeaderForHtmlReturnsPlainText()
{
// Arrange
var builder = new WebHostBuilder()
.Configure(app =>
{
app.UseDeveloperExceptionPage();
app.Run(context =>
{
throw new Exception("Test exception");
});
});
var server = new TestServer(builder);
// Act
var response = await server.CreateClient().GetAsync("/path");
// Assert
var responseText = await response.Content.ReadAsStringAsync();
Assert.Equal("text/plain", response.Content.Headers.ContentType.MediaType);
Assert.Contains("Test exception", responseText);
Assert.DoesNotContain("<html", responseText);
}
[Fact] [Fact]
public async Task ExceptionPageFiltersAreApplied() public async Task ExceptionPageFiltersAreApplied()
{ {

View File

@ -39,6 +39,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Client = factory Client = factory
.WithWebHostBuilder(builder => builder.ConfigureLogging(l => l.Services.AddSingleton<ILoggerFactory>(loggerProvider))) .WithWebHostBuilder(builder => builder.ConfigureLogging(l => l.Services.AddSingleton<ILoggerFactory>(loggerProvider)))
.CreateDefaultClient(); .CreateDefaultClient();
// These tests want to verify runtime compilation and formatting in the HTML of the error page
Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html"));
} }
public HttpClient Client { get; } public HttpClient Client { get; }