New ErrorHandler middleware.

This commit is contained in:
Chris Ross 2014-10-09 14:51:14 -07:00
parent 3279c15047
commit a0f3560095
11 changed files with 288 additions and 1 deletions

View File

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

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>427cdb36-78b0-4583-9ebc-7f283de60355</ProjectGuid>
<OutputType>Web</OutputType>
<RootNamespace>ErrorHandlerSample</RootNamespace>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>54230</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -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("<html><body>\r\n");
await context.Response.WriteAsync("We're sorry, we encountered an un-expected issue with your application.<br>\r\n");
var error = context.GetFeature<IErrorHandlerFeature>();
if (error != null)
{
// This error would not normally be exposed to the client
await context.Response.WriteAsync("<br>Error: " + System.Net.WebUtility.HtmlEncode(error.Error.Message) + "<br>\r\n");
}
await context.Response.WriteAsync("<br><a href=\"/\">Home</a><br>\r\n");
await context.Response.WriteAsync("</body></html>\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("<html><body>Welcome to the sample<br><br>\r\n");
await context.Response.WriteAsync("Click here to throw an exception: <a href=\"/throw\">throw</a>\r\n");
await context.Response.WriteAsync("</body></html>\r\n");
});
}
}
}

View File

@ -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" : { }
}
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
You've reached the static error page.<br /><br />
<a href="/">Home</a><br />
</body>
</html>

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <param name="app"></param>
/// <param name="errorHandlingPath"></param>
/// <returns></returns>
public static IApplicationBuilder UseErrorHandler(this IApplicationBuilder app, string errorHandlingPath)
{
var options = new ErrorHandlerOptions()
{
ErrorHandlingPath = new PathString(errorHandlingPath)
};
return app.UseMiddleware<ErrorHandlerMiddleware>(options);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="app"></param>
/// <param name="configure"></param>
/// <returns></returns>
public static IApplicationBuilder UseErrorHandler(this IApplicationBuilder app, Action<IApplicationBuilder> configure)
{
var subAppBuilder = app.New();
configure(subAppBuilder);
var errorPipeline = subAppBuilder.Build();
var options = new ErrorHandlerOptions()
{
ErrorHandler = errorPipeline
};
return app.UseMiddleware<ErrorHandlerMiddleware>(options);
}
}
}

View File

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

View File

@ -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<ErrorHandlerMiddleware>();
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<IErrorHandlerFeature>(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
}
}
}
}

View File

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

View File

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

View File

@ -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": {