From a0f3560095306d1349dff1d72e50f8702734613c Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Thu, 9 Oct 2014 14:51:14 -0700 Subject: [PATCH] New ErrorHandler middleware. --- DiagnosticsPages.sln | 15 +++- .../ErrorHandlerSample.kproj | 18 +++++ samples/ErrorHandlerSample/Startup.cs | 56 +++++++++++++ samples/ErrorHandlerSample/project.json | 18 +++++ samples/ErrorHandlerSample/wwwroot/error.html | 12 +++ .../ErrorHandlerExtensions.cs | 47 +++++++++++ .../ErrorHandlerFeature.cs | 12 +++ .../ErrorHandlerMiddleware.cs | 80 +++++++++++++++++++ .../ErrorHandlerOptions.cs | 15 ++++ .../IErrorHandlerFeature.cs | 14 ++++ src/Microsoft.AspNet.Diagnostics/project.json | 2 + 11 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 samples/ErrorHandlerSample/ErrorHandlerSample.kproj create mode 100644 samples/ErrorHandlerSample/Startup.cs create mode 100644 samples/ErrorHandlerSample/project.json create mode 100644 samples/ErrorHandlerSample/wwwroot/error.html create mode 100644 src/Microsoft.AspNet.Diagnostics/ErrorHandlerExtensions.cs create mode 100644 src/Microsoft.AspNet.Diagnostics/ErrorHandlerFeature.cs create mode 100644 src/Microsoft.AspNet.Diagnostics/ErrorHandlerMiddleware.cs create mode 100644 src/Microsoft.AspNet.Diagnostics/ErrorHandlerOptions.cs create mode 100644 src/Microsoft.AspNet.Diagnostics/IErrorHandlerFeature.cs diff --git a/DiagnosticsPages.sln b/DiagnosticsPages.sln index 9691d84257..1850737a64 100644 --- a/DiagnosticsPages.sln +++ b/DiagnosticsPages.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22130.0 +VisualStudioVersion = 14.0.22129.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{509A6F36-AD80-4A18-B5B1-717D38DFF29D}" EndProject @@ -26,6 +26,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{2AF90579-B EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Diagnostics.Tests", "test\Microsoft.AspNet.Diagnostics.Tests\Microsoft.AspNet.Diagnostics.Tests.kproj", "{994351B4-7B2A-4139-8B72-72C5BB5CC618}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ErrorHandlerSample", "samples\ErrorHandlerSample\ErrorHandlerSample.kproj", "{427CDB36-78B0-4583-9EBC-7F283DE60355}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -96,6 +98,16 @@ Global {994351B4-7B2A-4139-8B72-72C5BB5CC618}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {994351B4-7B2A-4139-8B72-72C5BB5CC618}.Release|Mixed Platforms.Build.0 = Release|Any CPU {994351B4-7B2A-4139-8B72-72C5BB5CC618}.Release|x86.ActiveCfg = Release|Any CPU + {427CDB36-78B0-4583-9EBC-7F283DE60355}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {427CDB36-78B0-4583-9EBC-7F283DE60355}.Debug|Any CPU.Build.0 = Debug|Any CPU + {427CDB36-78B0-4583-9EBC-7F283DE60355}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {427CDB36-78B0-4583-9EBC-7F283DE60355}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {427CDB36-78B0-4583-9EBC-7F283DE60355}.Debug|x86.ActiveCfg = Debug|Any CPU + {427CDB36-78B0-4583-9EBC-7F283DE60355}.Release|Any CPU.ActiveCfg = Release|Any CPU + {427CDB36-78B0-4583-9EBC-7F283DE60355}.Release|Any CPU.Build.0 = Release|Any CPU + {427CDB36-78B0-4583-9EBC-7F283DE60355}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {427CDB36-78B0-4583-9EBC-7F283DE60355}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {427CDB36-78B0-4583-9EBC-7F283DE60355}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -107,5 +119,6 @@ Global {4D4A785A-ECB9-4916-A88F-0FD306EE3B74} = {509A6F36-AD80-4A18-B5B1-717D38DFF29D} {CD62A191-39F5-4C86-BC1D-7731085120F5} = {ACAA0157-A8C4-4152-93DE-90CCDF304087} {994351B4-7B2A-4139-8B72-72C5BB5CC618} = {2AF90579-B118-4583-AE88-672EFACB5BC4} + {427CDB36-78B0-4583-9EBC-7F283DE60355} = {ACAA0157-A8C4-4152-93DE-90CCDF304087} EndGlobalSection EndGlobal diff --git a/samples/ErrorHandlerSample/ErrorHandlerSample.kproj b/samples/ErrorHandlerSample/ErrorHandlerSample.kproj new file mode 100644 index 0000000000..b863c76805 --- /dev/null +++ b/samples/ErrorHandlerSample/ErrorHandlerSample.kproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 427cdb36-78b0-4583-9ebc-7f283de60355 + Web + ErrorHandlerSample + + + 2.0 + 54230 + + + \ No newline at end of file diff --git a/samples/ErrorHandlerSample/Startup.cs b/samples/ErrorHandlerSample/Startup.cs new file mode 100644 index 0000000000..7ba297860a --- /dev/null +++ b/samples/ErrorHandlerSample/Startup.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Diagnostics; +using Microsoft.AspNet.Http; + +namespace ErrorHandlerSample +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + // Configure the error handler to show an error page. + app.UseErrorHandler(errorApp => + { + // Normally you'd use MVC or similar to render a nice page. + errorApp.Run(async context => + { + context.Response.StatusCode = 500; + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync("\r\n"); + await context.Response.WriteAsync("We're sorry, we encountered an un-expected issue with your application.
\r\n"); + + var error = context.GetFeature(); + if (error != null) + { + // This error would not normally be exposed to the client + await context.Response.WriteAsync("
Error: " + System.Net.WebUtility.HtmlEncode(error.Error.Message) + "
\r\n"); + } + await context.Response.WriteAsync("
Home
\r\n"); + await context.Response.WriteAsync("\r\n"); + await context.Response.WriteAsync(new string(' ', 512)); // Padding for IE + }); + }); + + // We could also configure it to re-execute the request on the normal pipeline with a different path. + // app.UseErrorHandler("/error.html"); + + // The broken section of our application. + app.Map("/throw", throwApp => + { + throwApp.Run(context => { throw new Exception("Application Exception"); }); + }); + + app.UseStaticFiles(); + + // The home page. + app.Run(async context => + { + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync("Welcome to the sample

\r\n"); + await context.Response.WriteAsync("Click here to throw an exception: throw\r\n"); + await context.Response.WriteAsync("\r\n"); + }); + } + } +} diff --git a/samples/ErrorHandlerSample/project.json b/samples/ErrorHandlerSample/project.json new file mode 100644 index 0000000000..a620f4479d --- /dev/null +++ b/samples/ErrorHandlerSample/project.json @@ -0,0 +1,18 @@ +{ + "webroot": "wwwroot", + "exclude": "wwwroot/**/*.*", + "dependencies": { + "Microsoft.AspNet.Diagnostics": "1.0.0-*", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-*" + }, + "commands": { + /* Change the port number when you are self hosting this application */ + "web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5000" + }, + "frameworks": { + "aspnet50" : { }, + "aspnetcore50" : { } + } +} diff --git a/samples/ErrorHandlerSample/wwwroot/error.html b/samples/ErrorHandlerSample/wwwroot/error.html new file mode 100644 index 0000000000..b9989e017f --- /dev/null +++ b/samples/ErrorHandlerSample/wwwroot/error.html @@ -0,0 +1,12 @@ + + + + + + + + + You've reached the static error page.

+ Home
+ + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics/ErrorHandlerExtensions.cs b/src/Microsoft.AspNet.Diagnostics/ErrorHandlerExtensions.cs new file mode 100644 index 0000000000..22bbe8512b --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics/ErrorHandlerExtensions.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Diagnostics; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Builder +{ + public static class ErrorHandlerExtensions + { + /// + /// Adds a middleware to the pipeline that will catch exceptions, log them, reset the request path, and re-execute the request. + /// The request will not be re-executed if the response has already started. + /// + /// + /// + /// + public static IApplicationBuilder UseErrorHandler(this IApplicationBuilder app, string errorHandlingPath) + { + var options = new ErrorHandlerOptions() + { + ErrorHandlingPath = new PathString(errorHandlingPath) + }; + return app.UseMiddleware(options); + } + + /// + /// Adds a middleware to the pipeline that will catch exceptions, log them, and re-execute the request in an alternate pipeline. + /// The request will not be re-executed if the response has already started. + /// + /// + /// + /// + public static IApplicationBuilder UseErrorHandler(this IApplicationBuilder app, Action configure) + { + var subAppBuilder = app.New(); + configure(subAppBuilder); + var errorPipeline = subAppBuilder.Build(); + var options = new ErrorHandlerOptions() + { + ErrorHandler = errorPipeline + }; + return app.UseMiddleware(options); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics/ErrorHandlerFeature.cs b/src/Microsoft.AspNet.Diagnostics/ErrorHandlerFeature.cs new file mode 100644 index 0000000000..bbac79bdc4 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics/ErrorHandlerFeature.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNet.Diagnostics +{ + public class ErrorHandlerFeature : IErrorHandlerFeature + { + public Exception Error { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics/ErrorHandlerMiddleware.cs b/src/Microsoft.AspNet.Diagnostics/ErrorHandlerMiddleware.cs new file mode 100644 index 0000000000..ab6790aadf --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics/ErrorHandlerMiddleware.cs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.Framework.Logging; + +namespace Microsoft.AspNet.Diagnostics +{ + public class ErrorHandlerMiddleware + { + private readonly RequestDelegate _next; + private readonly ErrorHandlerOptions _options; + private readonly ILogger _logger; + + public ErrorHandlerMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, ErrorHandlerOptions options) + { + _next = next; + _options = options; + _logger = loggerFactory.Create(); + if (_options.ErrorHandler == null) + { + _options.ErrorHandler = _next; + } + } + + public async Task Invoke(HttpContext context) + { + var responseStarted = false; + try + { + context.Response.OnSendingHeaders(state => responseStarted = true, null); + await _next(context); + } + catch (Exception ex) + { + _logger.WriteError("An unhandled exception has occurred: " + ex.Message, ex); + // We can't do anything if the response has already started, just abort. + if (responseStarted) + { + _logger.WriteWarning("The response has already started, the error handler will not be executed."); + throw; + } + + PathString originalPath = context.Request.Path; + if (_options.ErrorHandlingPath.HasValue) + { + context.Request.Path = _options.ErrorHandlingPath; + } + try + { + var errorHandlerFeature = new ErrorHandlerFeature() + { + Error = ex, + }; + context.SetFeature(errorHandlerFeature); + context.Response.StatusCode = 500; + context.Response.Headers.Clear(); + // TODO: Try clearing any buffered data. The buffering feature/middleware has not been designed yet. + await _options.ErrorHandler(context); + // TODO: Optional re-throw? We'll re-throw the original exception by default if the error handler throws. + return; + } + catch (Exception ex2) + { + // Suppress secondary exceptions, re-throw the original. + _logger.WriteError("An exception was thrown attempting to execute the error handler.", ex2); + } + finally + { + context.Request.Path = originalPath; + } + + throw; // Re-throw the original if we couldn't handle it + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics/ErrorHandlerOptions.cs b/src/Microsoft.AspNet.Diagnostics/ErrorHandlerOptions.cs new file mode 100644 index 0000000000..706127a5d5 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics/ErrorHandlerOptions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Diagnostics +{ + public class ErrorHandlerOptions + { + public PathString ErrorHandlingPath { get; set; } + + public RequestDelegate ErrorHandler { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics/IErrorHandlerFeature.cs b/src/Microsoft.AspNet.Diagnostics/IErrorHandlerFeature.cs new file mode 100644 index 0000000000..b53949b2a9 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics/IErrorHandlerFeature.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Framework.Runtime; + +namespace Microsoft.AspNet.Diagnostics +{ + [AssemblyNeutral] + public interface IErrorHandlerFeature + { + Exception Error { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics/project.json b/src/Microsoft.AspNet.Diagnostics/project.json index 49907f1e8f..7f2747a35f 100644 --- a/src/Microsoft.AspNet.Diagnostics/project.json +++ b/src/Microsoft.AspNet.Diagnostics/project.json @@ -4,6 +4,8 @@ "Microsoft.AspNet.FeatureModel": "1.0.0-*", "Microsoft.AspNet.Http": "1.0.0-*", "Microsoft.AspNet.PipelineCore": "1.0.0-*", + "Microsoft.AspNet.RequestContainer": "1.0.0-*", + "Microsoft.Framework.Logging": "1.0.0-*", "Microsoft.Framework.Runtime.Interfaces": { "version": "1.0.0-*", "type": "build" } }, "frameworks": {