From cd238d454555ad9254efda50ff2d56d3f41aeb07 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Tue, 27 Jan 2015 10:47:23 -0800 Subject: [PATCH] Outline for StatusCodePages middleware. --- DiagnosticsPages.sln | 16 +++ samples/ErrorPageSample/project.json | 2 +- samples/StatusCodePagesSample/Startup.cs | 75 ++++++++++ .../StatusCodePagesSample.kproj | 24 ++++ samples/StatusCodePagesSample/project.json | 24 ++++ .../IStatusCodeReExecuteFeature.cs | 15 ++ .../ReasonPhraseHelper.cs | 66 +++++++++ .../StatusCodeContext.cs | 24 ++++ .../StatusCodePagesExtensions.cs | 133 ++++++++++++++++++ .../StatusCodePagesMiddleware.cs | 44 ++++++ .../StatusCodePagesOptions.cs | 28 ++++ .../StatusCodeReExecuteFeature.cs | 12 ++ 12 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 samples/StatusCodePagesSample/Startup.cs create mode 100644 samples/StatusCodePagesSample/StatusCodePagesSample.kproj create mode 100644 samples/StatusCodePagesSample/project.json create mode 100644 src/Microsoft.AspNet.Diagnostics/IStatusCodeReExecuteFeature.cs create mode 100644 src/Microsoft.AspNet.Diagnostics/ReasonPhraseHelper.cs create mode 100644 src/Microsoft.AspNet.Diagnostics/StatusCodeContext.cs create mode 100644 src/Microsoft.AspNet.Diagnostics/StatusCodePagesExtensions.cs create mode 100644 src/Microsoft.AspNet.Diagnostics/StatusCodePagesMiddleware.cs create mode 100644 src/Microsoft.AspNet.Diagnostics/StatusCodePagesOptions.cs create mode 100644 src/Microsoft.AspNet.Diagnostics/StatusCodeReExecuteFeature.cs diff --git a/DiagnosticsPages.sln b/DiagnosticsPages.sln index bd8386853a..a04d598db8 100644 --- a/DiagnosticsPages.sln +++ b/DiagnosticsPages.sln @@ -33,8 +33,11 @@ EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Diagnostics.Entity.Tests", "test\Microsoft.AspNet.Diagnostics.Entity.Tests\Microsoft.AspNet.Diagnostics.Entity.Tests.kproj", "{5486117B-A742-49E0-94FC-12B76F061803}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Diagnostics.Entity.FunctionalTests", "test\Microsoft.AspNet.Diagnostics.Entity.FunctionalTests\Microsoft.AspNet.Diagnostics.Entity.FunctionalTests.kproj", "{2F9B479D-8247-4210-804B-78E6DD5C3E98}" +EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Diagnostics.Elm", "src\Microsoft.AspNet.Diagnostics.Elm\Microsoft.AspNet.Diagnostics.Elm.kproj", "{624B0019-956A-4157-B008-270C5B229553}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "StatusCodePagesSample", "samples\StatusCodePagesSample\StatusCodePagesSample.kproj", "{CC1F5841-FE10-4DDB-8477-C4DE92BA759F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -163,6 +166,18 @@ Global {624B0019-956A-4157-B008-270C5B229553}.Release|Mixed Platforms.Build.0 = Release|Any CPU {624B0019-956A-4157-B008-270C5B229553}.Release|x86.ActiveCfg = Release|Any CPU {624B0019-956A-4157-B008-270C5B229553}.Release|x86.Build.0 = Release|Any CPU + {CC1F5841-FE10-4DDB-8477-C4DE92BA759F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC1F5841-FE10-4DDB-8477-C4DE92BA759F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC1F5841-FE10-4DDB-8477-C4DE92BA759F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {CC1F5841-FE10-4DDB-8477-C4DE92BA759F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {CC1F5841-FE10-4DDB-8477-C4DE92BA759F}.Debug|x86.ActiveCfg = Debug|Any CPU + {CC1F5841-FE10-4DDB-8477-C4DE92BA759F}.Debug|x86.Build.0 = Debug|Any CPU + {CC1F5841-FE10-4DDB-8477-C4DE92BA759F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC1F5841-FE10-4DDB-8477-C4DE92BA759F}.Release|Any CPU.Build.0 = Release|Any CPU + {CC1F5841-FE10-4DDB-8477-C4DE92BA759F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {CC1F5841-FE10-4DDB-8477-C4DE92BA759F}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {CC1F5841-FE10-4DDB-8477-C4DE92BA759F}.Release|x86.ActiveCfg = Release|Any CPU + {CC1F5841-FE10-4DDB-8477-C4DE92BA759F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -179,5 +194,6 @@ Global {5486117B-A742-49E0-94FC-12B76F061803} = {2AF90579-B118-4583-AE88-672EFACB5BC4} {2F9B479D-8247-4210-804B-78E6DD5C3E98} = {2AF90579-B118-4583-AE88-672EFACB5BC4} {624B0019-956A-4157-B008-270C5B229553} = {509A6F36-AD80-4A18-B5B1-717D38DFF29D} + {CC1F5841-FE10-4DDB-8477-C4DE92BA759F} = {ACAA0157-A8C4-4152-93DE-90CCDF304087} EndGlobalSection EndGlobal diff --git a/samples/ErrorPageSample/project.json b/samples/ErrorPageSample/project.json index 8f4df8d60f..0de17aa1f1 100644 --- a/samples/ErrorPageSample/project.json +++ b/samples/ErrorPageSample/project.json @@ -1,6 +1,6 @@ { "dependencies": { - "Microsoft.AspNet.Diagnostics": "", + "Microsoft.AspNet.Diagnostics": "1.0.0-*", "Microsoft.AspNet.Server.WebListener": "1.0.0-*", "Microsoft.AspNet.Server.IIS": "1.0.0-*" }, diff --git a/samples/StatusCodePagesSample/Startup.cs b/samples/StatusCodePagesSample/Startup.cs new file mode 100644 index 0000000000..dbe85fa3f4 --- /dev/null +++ b/samples/StatusCodePagesSample/Startup.cs @@ -0,0 +1,75 @@ +// 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.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Diagnostics; +using Microsoft.AspNet.Http; + +namespace StatusCodePagesSample +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + app.UseErrorPage(ErrorPageOptions.ShowAll); + app.UseStatusCodePages(new StatusCodePagesOptions() // There is a default response but any of the following can be used to change the behavior. + // .WithHandler(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain")) + // .WithResponse("text/plain", "Response, status code: {0}") + // .WithRedirect("~/errors/{0}") // PathBase relative + // .WithRedirect("/base/errors/{0}") // Absolute + // .WithTangent(builder => builder.UseWelcomePage()) + // .WithReExecute("/errors/{0}") + ); + + // "/[?statuscode=400]" + app.Use((context, next) => + { + if (context.Request.Path.HasValue && !context.Request.Path.Equals(new PathString("/"))) + { + return next(); + } + + // Check for ?statuscode=400 + var requestedStatusCode = context.Request.Query["statuscode"]; + if (!string.IsNullOrEmpty(requestedStatusCode)) + { + context.Response.StatusCode = int.Parse(requestedStatusCode); + return Task.FromResult(0); + } + + var builder = new StringBuilder(); + builder.AppendLine(""); + builder.AppendLine("" + context.Request.PathBase + "/missingpage/
"); + for (int statusCode = 400; statusCode < 600; statusCode++) + { + builder.AppendLine("" + statusCode + "
"); + } + builder.AppendLine(""); + return context.Response.SendAsync(builder.ToString(), "text/html"); + }); + + // "/errors/400" + app.Use((context, next) => + { + PathString remainder; + if (context.Request.Path.StartsWithSegments(new PathString("/errors"), out remainder)) + { + var builder = new StringBuilder(); + builder.AppendLine(""); + builder.AppendLine("An error occurred, Status Code: " + WebUtility.HtmlEncode(remainder.ToString().Substring(1)) + "
"); + var referrer = context.Request.Headers["referer"]; + if (!string.IsNullOrEmpty(referrer)) + { + builder.AppendLine("Retry " + WebUtility.HtmlEncode(referrer) + "
"); + } + builder.AppendLine(""); + return context.Response.SendAsync(builder.ToString(), "text/html"); + } + return next(); + }); + } + } +} diff --git a/samples/StatusCodePagesSample/StatusCodePagesSample.kproj b/samples/StatusCodePagesSample/StatusCodePagesSample.kproj new file mode 100644 index 0000000000..32a014621a --- /dev/null +++ b/samples/StatusCodePagesSample/StatusCodePagesSample.kproj @@ -0,0 +1,24 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + cc1f5841-fe10-4ddb-8477-c4de92ba759f + StatusCodePagesSample + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 21211 + + + + + + + + \ No newline at end of file diff --git a/samples/StatusCodePagesSample/project.json b/samples/StatusCodePagesSample/project.json new file mode 100644 index 0000000000..75974480ec --- /dev/null +++ b/samples/StatusCodePagesSample/project.json @@ -0,0 +1,24 @@ +{ + "webroot": "wwwroot", + "version": "1.0.0-*", + "exclude": [ + "wwwroot" + ], + "packExclude": [ + "node_modules", + "bower_components", + "**.kproj", + "**.user", + "**.vspscc" + ], + "commands": { "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001/base" }, + "dependencies": { + "Microsoft.AspNet.Diagnostics": "1.0.0-*", + "Microsoft.AspNet.Server.IIS": "1.0.0-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*" + }, + "frameworks" : { + "aspnet50" : { }, + "aspnetcore50" : { } + } +} diff --git a/src/Microsoft.AspNet.Diagnostics/IStatusCodeReExecuteFeature.cs b/src/Microsoft.AspNet.Diagnostics/IStatusCodeReExecuteFeature.cs new file mode 100644 index 0000000000..241b73b9af --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics/IStatusCodeReExecuteFeature.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.Framework.Runtime; + +namespace Microsoft.AspNet.Diagnostics +{ + [AssemblyNeutral] + public interface IStatusCodeReExecuteFeature + { + string OriginalPathBase { get; set; } + + string OriginalPath { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics/ReasonPhraseHelper.cs b/src/Microsoft.AspNet.Diagnostics/ReasonPhraseHelper.cs new file mode 100644 index 0000000000..9dcb5e363b --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics/ReasonPhraseHelper.cs @@ -0,0 +1,66 @@ +// 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.Collections.Generic; + +namespace Microsoft.AspNet.Diagnostics +{ + // TODO: Move this into Abstractions + public static class ReasonPhraseHelper + { + private static IDictionary Phrases = new Dictionary() + { + { 200, "OK" }, + { 201, "Created" }, + { 202, "Accepted" }, + { 203, "Non-Authoritative Information" }, + { 204, "No Content" }, + { 205, "Reset Content" }, + { 206, "Partial Content" }, + + { 300, "Multiple Choices" }, + { 301, "Moved Permanently" }, + { 302, "Found" }, + { 303, "See Other" }, + { 304, "Not Modified" }, + { 305, "Use Proxy" }, + { 306, "Switch Proxy" }, + { 307, "Temporary Redirect" }, + + { 400, "Bad Request" }, + { 401, "Unauthorized" }, + { 402, "Payment Required" }, + { 403, "Forbidden" }, + { 404, "Not Found" }, + { 405, "Method Not Allowed" }, + { 406, "Not Acceptable" }, + { 407, "Proxy Authentication Required" }, + { 408, "Request Timeout" }, + { 409, "Conflict" }, + { 410, "Gone" }, + { 411, "Length Required" }, + { 412, "Precondition Failed" }, + { 413, "Request Entity Too Large" }, + { 414, "Request-URI Too Long" }, + { 415, "Unsupported Media Type" }, + { 416, "Requested Range Not Satisfiable" }, + { 417, "Expectation Failed" }, + { 418, "I'm a teapot" }, + { 419, "Authentication Timeout" }, + + { 500, "Internal Server Error" }, + { 501, "Not Implemented" }, + { 502, "Bad Gateway" }, + { 503, "Service Unavailable" }, + { 504, "Gateway Timeout" }, + { 505, "HTTP Version Not Supported" }, + { 506, "Variant Also Negotiates" }, + }; + + public static string GetReasonPhrase(int statusCode) + { + string phrase; + return Phrases.TryGetValue(statusCode, out phrase) ? phrase : string.Empty; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics/StatusCodeContext.cs b/src/Microsoft.AspNet.Diagnostics/StatusCodeContext.cs new file mode 100644 index 0000000000..5d673fd055 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics/StatusCodeContext.cs @@ -0,0 +1,24 @@ +// 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 StatusCodeContext + { + public StatusCodeContext(HttpContext context, StatusCodePagesOptions options, RequestDelegate next) + { + HttpContext = context; + Options = options; + Next = next; + } + + public HttpContext HttpContext { get; private set; } + + public StatusCodePagesOptions Options { get; private set; } + + public RequestDelegate Next { get; private set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics/StatusCodePagesExtensions.cs b/src/Microsoft.AspNet.Diagnostics/StatusCodePagesExtensions.cs new file mode 100644 index 0000000000..0656c5b525 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics/StatusCodePagesExtensions.cs @@ -0,0 +1,133 @@ +// 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.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNet.Diagnostics; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Builder +{ + public static class StatusCodePagesExtensions + { + /// + /// Adds a middleware that checks for responses with status codes between 400 and 599 that do not + /// have a body. Several approaches can be used to generate the response body. + /// + /// + /// + /// + public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, StatusCodePagesOptions options) + { + return app.UseMiddleware(options); + } + + /// + /// Specifies the handler to invoke to generate the response body. + /// + /// + /// + /// + public static StatusCodePagesOptions WithHandler(this StatusCodePagesOptions options, Func handler) + { + options.HandleAsync = handler; + return options; + } + + /// + /// Specifies the response body to send. This may include a '{0}' placeholder for the status code. + /// + /// + /// + /// + /// + public static StatusCodePagesOptions WithResponse(this StatusCodePagesOptions options, string contentType, string bodyFormat) + { + return options.WithHandler(context => + { + var body = string.Format(CultureInfo.InvariantCulture, bodyFormat, context.HttpContext.Response.StatusCode); + return context.HttpContext.Response.SendAsync(body, contentType); + }); + } + + /// + /// Specifies that responses should be handled by redirecting with the given location url template. + /// This may include a '{0}' placeholder for the status code. Ulrs starting with '~' will have PathBase prepeneded, + /// where any other url will be used as is. + /// + /// + /// + /// + public static StatusCodePagesOptions WithRedirect(this StatusCodePagesOptions options, string locationFormat) + { + if (locationFormat.StartsWith("~")) + { + locationFormat = locationFormat.Substring(1); + return options.WithHandler(context => + { + var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode); + context.HttpContext.Response.Redirect(context.HttpContext.Request.PathBase + location); + return Task.FromResult(0); + }); + } + else + { + return options.WithHandler(context => + { + var location = string.Format(CultureInfo.InvariantCulture, locationFormat, context.HttpContext.Response.StatusCode); + context.HttpContext.Response.Redirect(location); + return Task.FromResult(0); + }); + } + } + + /// + /// Specifies an alternate middleware pipeline to execute to generate the response body. + /// + /// + /// + /// + public static StatusCodePagesOptions WithTangent(this StatusCodePagesOptions options, Action configuration) + { + var builder = new ApplicationBuilder(serviceProvider: null); // TODO: services + configuration(builder); + var tangent = builder.Build(); + return options.WithHandler(context => tangent(context.HttpContext)); + } + + /// + /// Specifies that the response body should be generated by re-executing the request pipeline using an alternate path. + /// This path may contain a '{0}' placeholder of the status code. + /// + /// + /// + /// + public static StatusCodePagesOptions WithReExecute(this StatusCodePagesOptions options, string pathFormat) + { + return options.WithHandler(async context => + { + var newPath = new PathString(string.Format(CultureInfo.InvariantCulture, pathFormat, context.HttpContext.Response.StatusCode)); + + var originalPath = context.HttpContext.Request.Path; + // Store the original paths so the app can check it. + context.HttpContext.SetFeature(new StatusCodeReExecuteFeature() + { + OriginalPathBase = context.HttpContext.Request.PathBase.Value, + OriginalPath = originalPath.Value, + }); + + context.HttpContext.Request.Path = newPath; + try + { + await context.Next(context.HttpContext); + } + finally + { + context.HttpContext.Request.Path = originalPath; + context.HttpContext.SetFeature(null); + } + }); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics/StatusCodePagesMiddleware.cs b/src/Microsoft.AspNet.Diagnostics/StatusCodePagesMiddleware.cs new file mode 100644 index 0000000000..32b95c470a --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics/StatusCodePagesMiddleware.cs @@ -0,0 +1,44 @@ +// 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; + +namespace Microsoft.AspNet.Diagnostics +{ + public class StatusCodePagesMiddleware + { + private readonly RequestDelegate _next; + private readonly StatusCodePagesOptions _options; + + public StatusCodePagesMiddleware(RequestDelegate next, StatusCodePagesOptions options) + { + _next = next; + _options = options; + if (_options.HandleAsync == null) + { + throw new ArgumentException("Missing HandleAsync implementation."); + } + } + + public async Task Invoke(HttpContext context) + { + await _next(context); + + // Do nothing if a response body has already been provided. + if (context.Response.HeadersSent + || context.Response.StatusCode < 400 + || context.Response.StatusCode >= 600 + || context.Response.ContentLength.HasValue + || !string.IsNullOrEmpty(context.Response.ContentType)) + { + return; + } + + var statusCodeContext = new StatusCodeContext(context, _options, _next); + await _options.HandleAsync(statusCodeContext); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics/StatusCodePagesOptions.cs b/src/Microsoft.AspNet.Diagnostics/StatusCodePagesOptions.cs new file mode 100644 index 0000000000..8b440a242d --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics/StatusCodePagesOptions.cs @@ -0,0 +1,28 @@ +// 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.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.Diagnostics +{ + public class StatusCodePagesOptions + { + public StatusCodePagesOptions() + { + HandleAsync = context => + { + // TODO: Render with a pre-compiled html razor view. + // Note the 500 spaces are to work around an IE 'feature' + var statusCode = context.HttpContext.Response.StatusCode; + var body = string.Format(CultureInfo.InvariantCulture, "Status Code: {0}; {1}", + statusCode, ReasonPhraseHelper.GetReasonPhrase(statusCode)) + new string(' ', 500); + return context.HttpContext.Response.SendAsync(body, "text/plain"); + }; + } + + public Func HandleAsync { get; set; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Diagnostics/StatusCodeReExecuteFeature.cs b/src/Microsoft.AspNet.Diagnostics/StatusCodeReExecuteFeature.cs new file mode 100644 index 0000000000..cb6c850df6 --- /dev/null +++ b/src/Microsoft.AspNet.Diagnostics/StatusCodeReExecuteFeature.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. + +namespace Microsoft.AspNet.Diagnostics +{ + public class StatusCodeReExecuteFeature : IStatusCodeReExecuteFeature + { + public string OriginalPath { get; set; } + + public string OriginalPathBase { get; set; } + } +} \ No newline at end of file