MVC Controller Response - Wrong ContentType #3245

This commit is contained in:
Kiran Challa 2015-12-14 15:11:49 -08:00
parent f56cf97805
commit d77655fb73
4 changed files with 139 additions and 14 deletions

View File

@ -48,7 +48,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>(); Logger = loggerFactory.CreateLogger<ObjectResultExecutor>();
WriterFactory = writerFactory.CreateWriter; WriterFactory = writerFactory.CreateWriter;
} }
/// <summary> /// <summary>
/// Gets the <see cref="ILogger"/>. /// Gets the <see cref="ILogger"/>.
/// </summary> /// </summary>
@ -89,6 +89,22 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
throw new ArgumentNullException(nameof(result)); throw new ArgumentNullException(nameof(result));
} }
// If the user sets the content type both on the ObjectResult (example: by Produces) and Response object,
// then the one set on ObjectResult takes precedence over the Response object
if (result.ContentTypes == null || result.ContentTypes.Count == 0)
{
var responseContentType = context.HttpContext.Response.ContentType;
if (!string.IsNullOrEmpty(responseContentType))
{
if (result.ContentTypes == null)
{
result.ContentTypes = new List<MediaTypeHeaderValue>();
}
result.ContentTypes.Add(MediaTypeHeaderValue.Parse(responseContentType));
}
}
ValidateContentTypes(result.ContentTypes); ValidateContentTypes(result.ContentTypes);
var formatters = result.Formatters; var formatters = result.Formatters;
@ -108,7 +124,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
WriterFactory, WriterFactory,
objectType, objectType,
result.Value); result.Value);
var selectedFormatter = SelectFormatter(formatterContext, result.ContentTypes, formatters); var selectedFormatter = SelectFormatter(formatterContext, result.ContentTypes, formatters);
if (selectedFormatter == null) if (selectedFormatter == null)
{ {
@ -121,7 +137,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
Logger.FormatterSelected(selectedFormatter, formatterContext); Logger.FormatterSelected(selectedFormatter, formatterContext);
Logger.ObjectResultExecuting(context); Logger.ObjectResultExecuting(context);
result.OnFormatting(context); result.OnFormatting(context);
return selectedFormatter.WriteAsync(formatterContext); return selectedFormatter.WriteAsync(formatterContext);
} }
@ -324,7 +340,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
{ {
throw new ArgumentNullException(nameof(sortedAcceptHeaders)); throw new ArgumentNullException(nameof(sortedAcceptHeaders));
} }
foreach (var contentType in sortedAcceptHeaders) foreach (var contentType in sortedAcceptHeaders)
{ {
foreach (var formatter in formatters) foreach (var formatter in formatters)
@ -466,7 +482,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
} }
} }
} }
// We want a descending sort, but BinarySearch does ascending // We want a descending sort, but BinarySearch does ascending
sorted.Reverse(); sorted.Reverse();
return sorted; return sorted;

View File

@ -51,6 +51,30 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
Assert.Equal(new MediaTypeHeaderValue("application/json"), context.ContentType); Assert.Equal(new MediaTypeHeaderValue("application/json"), context.ContentType);
} }
// For this test case probably the most common use case is when there is a format mapping based
// content type selected but the developer had set the content type on the Response.ContentType
[Fact]
public async Task ExecuteAsync_ContentTypeProvidedFromResponseAndObjectResult_UsesResponseContentType()
{
// Arrange
var executor = CreateCustomObjectResultExecutor();
var httpContext = new DefaultHttpContext();
var actionContext = new ActionContext() { HttpContext = httpContext };
httpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used
httpContext.Response.ContentType = "text/plain";
var result = new ObjectResult("input");
result.Formatters.Add(new TestXmlOutputFormatter());
result.Formatters.Add(new TestJsonOutputFormatter());
result.Formatters.Add(new TestStringOutputFormatter()); // This will be chosen based on the content type
// Act
await executor.ExecuteAsync(actionContext, result);
// Assert
Assert.IsType<TestStringOutputFormatter>(executor.SelectedOutputFormatter);
Assert.Equal("text/plain; charset=utf-8", httpContext.Response.ContentType);
}
[Fact] [Fact]
public void SelectFormatter_WithOneProvidedContentType_IgnoresAcceptHeader() public void SelectFormatter_WithOneProvidedContentType_IgnoresAcceptHeader()
{ {
@ -82,6 +106,27 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
Assert.Equal(new MediaTypeHeaderValue("application/json"), context.ContentType); Assert.Equal(new MediaTypeHeaderValue("application/json"), context.ContentType);
} }
[Fact]
public async Task ExecuteAsync_WithOneProvidedContentType_FromResponseContentType_IgnoresAcceptHeader()
{
// Arrange
var executor = CreateCustomObjectResultExecutor();
var httpContext = new DefaultHttpContext();
var actionContext = new ActionContext() { HttpContext = httpContext };
httpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used
httpContext.Response.ContentType = "application/json";
var result = new ObjectResult("input");
result.Formatters.Add(new TestXmlOutputFormatter());
result.Formatters.Add(new TestJsonOutputFormatter()); // This will be chosen based on the content type
// Act
await executor.ExecuteAsync(actionContext, result);
// Assert
Assert.IsType<TestJsonOutputFormatter>(executor.SelectedOutputFormatter);
Assert.Equal("application/json; charset=utf-8", httpContext.Response.ContentType);
}
[Fact] [Fact]
public void SelectFormatter_WithOneProvidedContentType_NoFallback() public void SelectFormatter_WithOneProvidedContentType_NoFallback()
{ {
@ -111,6 +156,25 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
Assert.Null(formatter); Assert.Null(formatter);
} }
[Fact]
public async Task ExecuteAsync_WithOneProvidedContentType_FromResponseContentType_NoFallback()
{
// Arrange
var executor = CreateCustomObjectResultExecutor();
var httpContext = new DefaultHttpContext();
var actionContext = new ActionContext() { HttpContext = httpContext };
httpContext.Request.Headers[HeaderNames.Accept] = "application/xml"; // This will not be used
httpContext.Response.ContentType = "application/json";
var result = new ObjectResult("input");
result.Formatters.Add(new TestXmlOutputFormatter());
// Act
await executor.ExecuteAsync(actionContext, result);
// Assert
Assert.Null(executor.SelectedOutputFormatter);
}
// ObjectResult.ContentTypes, Accept header, expected content type // ObjectResult.ContentTypes, Accept header, expected content type
public static TheoryData<string[], string, string> ContentTypes public static TheoryData<string[], string, string> ContentTypes
{ {
@ -158,7 +222,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
{ {
// Arrange // Arrange
var executor = CreateExecutor(); var executor = CreateExecutor();
var formatters = new List<IOutputFormatter> var formatters = new List<IOutputFormatter>
{ {
new CannotWriteFormatter(), new CannotWriteFormatter(),
@ -229,7 +293,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
var context = new OutputFormatterWriteContext( var context = new OutputFormatterWriteContext(
new DefaultHttpContext(), new DefaultHttpContext(),
new TestHttpResponseStreamWriterFactory().CreateWriter, new TestHttpResponseStreamWriterFactory().CreateWriter,
objectType: null, objectType: null,
@object: null); @object: null);
@ -315,7 +379,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
var executor = CreateExecutor(); var executor = CreateExecutor();
var actionContext = new ActionContext(); var actionContext = new ActionContext() { HttpContext = new DefaultHttpContext() };
// Act & Assert // Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>( var exception = await Assert.ThrowsAsync<InvalidOperationException>(
@ -331,7 +395,7 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
// Chrome & Opera // Chrome & Opera
[InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "application/json; charset=utf-8")] [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "application/json; charset=utf-8")]
// IE // IE
[InlineData("text/html, application/xhtml+xml, */*", "application/json; charset=utf-8")] [InlineData("text/html, application/xhtml+xml, */*", "application/json; charset=utf-8")]
// Firefox & Safari // Firefox & Safari
[InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "application/json; charset=utf-8")] [InlineData("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "application/json; charset=utf-8")]
// Misc // Misc
@ -428,6 +492,14 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
NullLoggerFactory.Instance); NullLoggerFactory.Instance);
} }
private static CustomObjectResultExecutor CreateCustomObjectResultExecutor()
{
return new CustomObjectResultExecutor(
new TestOptionsManager<MvcOptions>(),
new TestHttpResponseStreamWriterFactory(),
NullLoggerFactory.Instance);
}
private class CannotWriteFormatter : IOutputFormatter private class CannotWriteFormatter : IOutputFormatter
{ {
public virtual bool CanWriteResult(OutputFormatterCanWriteContext context) public virtual bool CanWriteResult(OutputFormatterCanWriteContext context)
@ -473,17 +545,32 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
} }
} }
private class TestStringOutputFormatter : OutputFormatter
{
public TestStringOutputFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
SupportedEncodings.Add(Encoding.UTF8);
}
public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
{
return Task.FromResult(0);
}
}
private class TestObjectResultExecutor : ObjectResultExecutor private class TestObjectResultExecutor : ObjectResultExecutor
{ {
public TestObjectResultExecutor( public TestObjectResultExecutor(
IOptions<MvcOptions> options, IOptions<MvcOptions> options,
IHttpResponseStreamWriterFactory writerFactory, IHttpResponseStreamWriterFactory writerFactory,
ILoggerFactory loggerFactory) ILoggerFactory loggerFactory)
: base(options, writerFactory, loggerFactory) : base(options, writerFactory, loggerFactory)
{ {
} }
public new IOutputFormatter SelectFormatter( new public IOutputFormatter SelectFormatter(
OutputFormatterWriteContext formatterContext, OutputFormatterWriteContext formatterContext,
IList<MediaTypeHeaderValue> contentTypes, IList<MediaTypeHeaderValue> contentTypes,
IList<IOutputFormatter> formatters) IList<IOutputFormatter> formatters)
@ -491,5 +578,27 @@ namespace Microsoft.AspNet.Mvc.Infrastructure
return base.SelectFormatter(formatterContext, contentTypes, formatters); return base.SelectFormatter(formatterContext, contentTypes, formatters);
} }
} }
private class CustomObjectResultExecutor : ObjectResultExecutor
{
public CustomObjectResultExecutor(
IOptions<MvcOptions> options,
IHttpResponseStreamWriterFactory writerFactory,
ILoggerFactory loggerFactory)
: base(options, writerFactory, loggerFactory)
{
}
public IOutputFormatter SelectedOutputFormatter { get; private set; }
protected override IOutputFormatter SelectFormatter(
OutputFormatterWriteContext formatterContext,
IList<MediaTypeHeaderValue> contentTypes,
IList<IOutputFormatter> formatters)
{
SelectedOutputFormatter = base.SelectFormatter(formatterContext, contentTypes, formatters);
return SelectedOutputFormatter;
}
}
} }
} }

View File

@ -21,7 +21,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
[InlineData("MemoryStreamWithContentType", "text/html")] [InlineData("MemoryStreamWithContentType", "text/html")]
[InlineData("MemoryStreamWithContentTypeFromProduces", "text/plain")] [InlineData("MemoryStreamWithContentTypeFromProduces", "text/plain")]
[InlineData("MemoryStreamWithContentTypeFromProducesWithMultipleValues", "text/html")] [InlineData("MemoryStreamWithContentTypeFromProducesWithMultipleValues", "text/html")]
[InlineData("MemoryStreamOverridesContentTypeWithProduces", "text/plain")] [InlineData("MemoryStreamOverridesProducesContentTypeWithResponseContentType", "text/plain")]
public async Task StreamOutputFormatter_ReturnsAppropriateContentAndContentType(string actionName, string contentType) public async Task StreamOutputFormatter_ReturnsAppropriateContentAndContentType(string actionName, string contentType)
{ {
// Arrange & Act // Arrange & Act

View File

@ -37,7 +37,7 @@ namespace FormatterWebSite
[HttpGet] [HttpGet]
[Produces("text/plain")] [Produces("text/plain")]
public Stream MemoryStreamOverridesContentTypeWithProduces() public Stream MemoryStreamOverridesProducesContentTypeWithResponseContentType()
{ {
// Produces will set a ContentType on the implicit ObjectResult and // Produces will set a ContentType on the implicit ObjectResult and
// ContentType on response are overriden by content types from ObjectResult. // ContentType on response are overriden by content types from ObjectResult.