Outline for StatusCodePages middleware.

This commit is contained in:
Chris Ross 2015-01-27 10:47:23 -08:00 committed by Praburaj
parent a09d8a4ead
commit cd238d4545
12 changed files with 462 additions and 1 deletions

View File

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

View File

@ -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-*"
},

View File

@ -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("<html><body>");
builder.AppendLine("<a href=\"" + context.Request.PathBase + "/missingpage/\">" + context.Request.PathBase + "/missingpage/</a><br>");
for (int statusCode = 400; statusCode < 600; statusCode++)
{
builder.AppendLine("<a href=\"?statuscode=" + statusCode + "\">" + statusCode + "</a><br>");
}
builder.AppendLine("</body></html>");
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("<html><body>");
builder.AppendLine("An error occurred, Status Code: " + WebUtility.HtmlEncode(remainder.ToString().Substring(1)) + "<br>");
var referrer = context.Request.Headers["referer"];
if (!string.IsNullOrEmpty(referrer))
{
builder.AppendLine("<a href=\"" + WebUtility.HtmlEncode(referrer) + "\">Retry " + WebUtility.HtmlEncode(referrer) + "</a><br>");
}
builder.AppendLine("</body></html>");
return context.Response.SendAsync(builder.ToString(), "text/html");
}
return next();
});
}
}
}

View File

@ -0,0 +1,24 @@
<?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>cc1f5841-fe10-4ddb-8477-c4de92ba759f</ProjectGuid>
<RootNamespace>StatusCodePagesSample</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\bin\$(MSBuildProjectName)\</OutputPath>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>21211</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
<ProjectExtensions>
<VisualStudio>
<UserProperties project_1json__JSONSchema="http://www.asp.net/media/4878834/project.json" />
</VisualStudio>
</ProjectExtensions>
</Project>

View File

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

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.Framework.Runtime;
namespace Microsoft.AspNet.Diagnostics
{
[AssemblyNeutral]
public interface IStatusCodeReExecuteFeature
{
string OriginalPathBase { get; set; }
string OriginalPath { get; set; }
}
}

View File

@ -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<int, string> Phrases = new Dictionary<int, string>()
{
{ 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;
}
}
}

View File

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

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <param name="app"></param>
/// <param name="options"></param>
/// <returns></returns>
public static IApplicationBuilder UseStatusCodePages(this IApplicationBuilder app, StatusCodePagesOptions options)
{
return app.UseMiddleware<StatusCodePagesMiddleware>(options);
}
/// <summary>
/// Specifies the handler to invoke to generate the response body.
/// </summary>
/// <param name="options"></param>
/// <param name="handler"></param>
/// <returns></returns>
public static StatusCodePagesOptions WithHandler(this StatusCodePagesOptions options, Func<StatusCodeContext, Task> handler)
{
options.HandleAsync = handler;
return options;
}
/// <summary>
/// Specifies the response body to send. This may include a '{0}' placeholder for the status code.
/// </summary>
/// <param name="options"></param>
/// <param name="contentType"></param>
/// <param name="bodyFormat"></param>
/// <returns></returns>
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);
});
}
/// <summary>
/// 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.
/// </summary>
/// <param name="options"></param>
/// <param name="locationFormat"></param>
/// <returns></returns>
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);
});
}
}
/// <summary>
/// Specifies an alternate middleware pipeline to execute to generate the response body.
/// </summary>
/// <param name="options"></param>
/// <param name="configuration"></param>
/// <returns></returns>
public static StatusCodePagesOptions WithTangent(this StatusCodePagesOptions options, Action<IApplicationBuilder> configuration)
{
var builder = new ApplicationBuilder(serviceProvider: null); // TODO: services
configuration(builder);
var tangent = builder.Build();
return options.WithHandler(context => tangent(context.HttpContext));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="options"></param>
/// <param name="pathFormat"></param>
/// <returns></returns>
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<IStatusCodeReExecuteFeature>(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<IStatusCodeReExecuteFeature>(null);
}
});
}
}
}

View File

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

View File

@ -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<StatusCodeContext, Task> HandleAsync { get; set; }
}
}

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.
namespace Microsoft.AspNet.Diagnostics
{
public class StatusCodeReExecuteFeature : IStatusCodeReExecuteFeature
{
public string OriginalPath { get; set; }
public string OriginalPathBase { get; set; }
}
}