From 236d4009c3e7509acabeedd31601046352b090a6 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Mon, 13 Jul 2015 15:21:00 -0700 Subject: [PATCH] [Fixes #40] Suppress caching for ErrorHandler --- .../ErrorHandlerMiddleware.cs | 15 +- .../ErrorHandlerTest.cs | 257 ++++++++++++++++++ .../project.json | 1 + 3 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 test/Microsoft.AspNet.Diagnostics.Tests/ErrorHandlerTest.cs diff --git a/src/Microsoft.AspNet.Diagnostics/ErrorHandlerMiddleware.cs b/src/Microsoft.AspNet.Diagnostics/ErrorHandlerMiddleware.cs index 677e0e690a..852f0a8539 100644 --- a/src/Microsoft.AspNet.Diagnostics/ErrorHandlerMiddleware.cs +++ b/src/Microsoft.AspNet.Diagnostics/ErrorHandlerMiddleware.cs @@ -14,6 +14,7 @@ namespace Microsoft.AspNet.Diagnostics private readonly RequestDelegate _next; private readonly ErrorHandlerOptions _options; private readonly ILogger _logger; + private readonly Func _clearCacheHeadersDelegate; public ErrorHandlerMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, ErrorHandlerOptions options) { @@ -24,6 +25,7 @@ namespace Microsoft.AspNet.Diagnostics { _options.ErrorHandler = _next; } + _clearCacheHeadersDelegate = ClearCacheHeaders; } public async Task Invoke(HttpContext context) @@ -56,6 +58,8 @@ namespace Microsoft.AspNet.Diagnostics context.SetFeature(errorHandlerFeature); context.Response.StatusCode = 500; context.Response.Headers.Clear(); + context.Response.OnStarting(_clearCacheHeadersDelegate, context.Response); + // 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. @@ -70,9 +74,18 @@ namespace Microsoft.AspNet.Diagnostics { context.Request.Path = originalPath; } - throw; // Re-throw the original if we couldn't handle it } } + + private Task ClearCacheHeaders(object state) + { + var response = (HttpResponse)state; + response.Headers.Set("Cache-Control", "no-cache"); + response.Headers.Set("Pragma", "no-cache"); + response.Headers.Set("Expires", "-1"); + response.Headers.Remove("ETag"); + return Task.FromResult(0); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/ErrorHandlerTest.cs b/test/Microsoft.AspNet.Diagnostics.Tests/ErrorHandlerTest.cs new file mode 100644 index 0000000000..0e54ca7b43 --- /dev/null +++ b/test/Microsoft.AspNet.Diagnostics.Tests/ErrorHandlerTest.cs @@ -0,0 +1,257 @@ +// Copyright (c) .NET Foundation. 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.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.Diagnostics +{ + public class ErrorHandlerTest + { + [Theory] + [InlineData(HttpStatusCode.NotFound)] + [InlineData(HttpStatusCode.BadRequest)] + [InlineData(HttpStatusCode.InternalServerError)] + public async Task OnlyHandles_UnhandledExceptions(HttpStatusCode expectedStatusCode) + { + using (var server = TestServer.Create(app => + { + app.UseErrorHandler("/handle-errors"); + + app.Map("/handle-errors", (innerAppBuilder) => + { + innerAppBuilder.Run(async (httpContext) => + { + await httpContext.Response.WriteAsync("Handled error in a custom way."); + }); + }); + + app.Run((RequestDelegate)(async (context) => + { + context.Response.StatusCode = (int)expectedStatusCode; + context.Response.ContentType = "text/plain; charset=utf-8"; + await context.Response.WriteAsync("An error occurred while adding a product"); + })); + })) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + Assert.Equal(expectedStatusCode, response.StatusCode); + Assert.Equal("An error occurred while adding a product", await response.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async Task DoesNotHandle_UnhandledExceptions_WhenResponseAlreadyStarted() + { + using (var server = TestServer.Create(app => + { + app.Use(async (httpContext, next) => + { + Exception exception = null; + try + { + await next(); + } + catch (InvalidOperationException ex) + { + exception = ex; + } + + Assert.NotNull(exception); + Assert.Equal("Something bad happened", exception.Message); + }); + + app.UseErrorHandler("/handle-errors"); + + app.Map("/handle-errors", (innerAppBuilder) => + { + innerAppBuilder.Run(async (httpContext) => + { + await httpContext.Response.WriteAsync("Handled error in a custom way."); + }); + }); + + app.Run(async (httpContext) => + { + await httpContext.Response.WriteAsync("Hello"); + throw new InvalidOperationException("Something bad happened"); + }); + })) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + Assert.Equal("Hello", await response.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async Task ClearsCacheHeaders_SetByReexecutionPathHandlers() + { + var expiresTime = DateTime.UtcNow.AddDays(5).ToString("R"); + var expectedResponseBody = "Handled error in a custom way."; + using (var server = TestServer.Create(app => + { + app.UseErrorHandler("/handle-errors"); + + app.Map("/handle-errors", (innerAppBuilder) => + { + innerAppBuilder.Run(async (httpContext) => + { + httpContext.Response.Headers.Add("Cache-Control", new[] { "max-age=600" }); + httpContext.Response.Headers.Add("Pragma", new[] { "max-age=600" }); + httpContext.Response.Headers.Add( + "Expires", new[] { expiresTime }); + httpContext.Response.Headers.Add("ETag", new[] { "12345" }); + + await httpContext.Response.WriteAsync(expectedResponseBody); + }); + }); + + app.Run((context) => + { + throw new InvalidOperationException("Invalid input provided."); + }); + })) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); + Assert.Equal(expectedResponseBody, await response.Content.ReadAsStringAsync()); + IEnumerable values; + Assert.True(response.Headers.TryGetValues("Cache-Control", out values)); + Assert.Single(values); + Assert.Equal("no-cache", values.First()); + Assert.True(response.Headers.TryGetValues("Pragma", out values)); + Assert.Single(values); + Assert.Equal("no-cache", values.First()); + Assert.True(response.Content.Headers.TryGetValues("Expires", out values)); + Assert.Single(values); + Assert.Equal("-1", values.First()); + Assert.False(response.Headers.TryGetValues("ETag", out values)); + } + } + + [Fact] + public async Task DoesNotModifyCacheHeaders_WhenNoExceptionIsThrown() + { + var expiresTime = DateTime.UtcNow.AddDays(10).ToString("R"); + var expectedResponseBody = "Hello world!"; + using (var server = TestServer.Create(app => + { + app.UseErrorHandler("/handle-errors"); + + app.Map("/handle-errors", (innerAppBuilder) => + { + innerAppBuilder.Run(async (httpContext) => + { + await httpContext.Response.WriteAsync("Handled error in a custom way."); + }); + }); + + app.Run(async (httpContext) => + { + httpContext.Response.Headers.Add("Cache-Control", new[] { "max-age=3600" }); + httpContext.Response.Headers.Add("Pragma", new[] { "max-age=3600" }); + httpContext.Response.Headers.Add("Expires", new[] { expiresTime }); + httpContext.Response.Headers.Add("ETag", new[] { "abcdef" }); + + await httpContext.Response.WriteAsync(expectedResponseBody); + }); + })) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + Assert.Equal(expectedResponseBody, await response.Content.ReadAsStringAsync()); + IEnumerable values; + Assert.True(response.Headers.TryGetValues("Cache-Control", out values)); + Assert.Single(values); + Assert.Equal("max-age=3600", values.First()); + Assert.True(response.Headers.TryGetValues("Pragma", out values)); + Assert.Single(values); + Assert.Equal("max-age=3600", values.First()); + Assert.True(response.Content.Headers.TryGetValues("Expires", out values)); + Assert.Single(values); + Assert.Equal(expiresTime, values.First()); + Assert.True(response.Headers.TryGetValues("ETag", out values)); + Assert.Single(values); + Assert.Equal("abcdef", values.First()); + } + } + + [Fact] + public async Task DoesNotClearCacheHeaders_WhenResponseHasAlreadyStarted() + { + var expiresTime = DateTime.UtcNow.AddDays(10).ToString("R"); + using (var server = TestServer.Create(app => + { + app.Use(async (httpContext, next) => + { + Exception exception = null; + try + { + await next(); + } + catch (InvalidOperationException ex) + { + exception = ex; + } + + Assert.NotNull(exception); + Assert.Equal("Something bad happened", exception.Message); + }); + + app.UseErrorHandler("/handle-errors"); + + app.Map("/handle-errors", (innerAppBuilder) => + { + innerAppBuilder.Run(async (httpContext) => + { + await httpContext.Response.WriteAsync("Handled error in a custom way."); + }); + }); + + app.Run(async (httpContext) => + { + httpContext.Response.Headers.Add("Cache-Control", new[] { "max-age=3600" }); + httpContext.Response.Headers.Add("Pragma", new[] { "max-age=3600" }); + httpContext.Response.Headers.Add("Expires", new[] { expiresTime }); + httpContext.Response.Headers.Add("ETag", new[] { "abcdef" }); + + await httpContext.Response.WriteAsync("Hello"); + + throw new InvalidOperationException("Something bad happened"); + }); + })) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + Assert.Equal("Hello", await response.Content.ReadAsStringAsync()); + IEnumerable values; + Assert.True(response.Headers.TryGetValues("Cache-Control", out values)); + Assert.Single(values); + Assert.Equal("max-age=3600", values.First()); + Assert.True(response.Headers.TryGetValues("Pragma", out values)); + Assert.Single(values); + Assert.Equal("max-age=3600", values.First()); + Assert.True(response.Content.Headers.TryGetValues("Expires", out values)); + Assert.Single(values); + Assert.Equal(expiresTime, values.First()); + Assert.True(response.Headers.TryGetValues("ETag", out values)); + Assert.Single(values); + Assert.Equal("abcdef", values.First()); + } + } + } +} diff --git a/test/Microsoft.AspNet.Diagnostics.Tests/project.json b/test/Microsoft.AspNet.Diagnostics.Tests/project.json index 2b5be2644b..3e2f021b66 100644 --- a/test/Microsoft.AspNet.Diagnostics.Tests/project.json +++ b/test/Microsoft.AspNet.Diagnostics.Tests/project.json @@ -4,6 +4,7 @@ }, "dependencies": { "Microsoft.AspNet.Diagnostics.Elm": "1.0.0-*", + "Microsoft.AspNet.TestHost": "1.0.0-*", "Microsoft.Framework.DependencyInjection": "1.0.0-*", "xunit.runner.aspnet": "2.0.0-aspnet-*" },