From 48f0a76ea9af868ad4d7dd3f22a93298502dbf36 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Tue, 16 Apr 2019 18:34:07 -0700 Subject: [PATCH] Support plain text exception formatting (#9342) - If there's no Accept: text/html then print the exception.ToString as plaintext --- .../DeveloperExceptionPageMiddleware.cs | 34 ++++++++++-- .../DeveloperExceptionPageMiddlewareTest.cs | 53 +++++++++++++++++++ .../Mvc.FunctionalTests/ErrorPageTests.cs | 2 + 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs index 22c7c39b47..c712d80362 100644 --- a/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs +++ b/src/Middleware/Diagnostics/src/DeveloperExceptionPage/DeveloperExceptionPageMiddleware.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.Internal; @@ -17,11 +18,12 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.StackTrace.Sources; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Diagnostics { /// - /// 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. /// public class DeveloperExceptionPageMiddleware { @@ -32,6 +34,7 @@ namespace Microsoft.AspNetCore.Diagnostics private readonly DiagnosticSource _diagnosticSource; private readonly ExceptionDetailsProvider _exceptionDetailsProvider; private readonly Func _exceptionHandler; + private static readonly MediaTypeHeaderValue _textPlainMediaType = new MediaTypeHeaderValue("text/html"); /// /// Initializes a new instance of the 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. private Task DisplayException(ErrorContext errorContext) { - var compilationException = errorContext.Exception as ICompilationException; - if (compilationException != null) + 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(_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( diff --git a/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs b/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs index 8552a2da8e..26bda1074d 100644 --- a/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs +++ b/src/Middleware/Diagnostics/test/UnitTests/DeveloperExceptionPageMiddlewareTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -45,6 +46,58 @@ namespace Microsoft.AspNetCore.Diagnostics 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(" + { + 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(" builder.ConfigureLogging(l => l.Services.AddSingleton(loggerProvider))) .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; }