Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Alter content negotiation algorithm so that it can be configured (via
Browse files Browse the repository at this point in the history
MvcOptions) to always respect an explicit Accept header. Fixes #4612.
  • Loading branch information
tuespetre committed Jun 7, 2016
1 parent 2e2784a commit 5a3875e
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 242 deletions.
Expand Up @@ -22,12 +22,6 @@ public abstract class OutputFormatterCanWriteContext
/// </remarks>
public virtual StringSegment ContentType { get; set; }

/// <summary>
/// Gets or sets a value indicating that content-negotiation could not find a formatter based on the
/// information on the <see cref="Http.HttpRequest"/>.
/// </summary>
public virtual bool? FailedContentNegotiation { get; set; }

/// <summary>
/// Gets or sets the object to write to the response.
/// </summary>
Expand Down

This file was deleted.

168 changes: 91 additions & 77 deletions src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectResultExecutor.cs
Expand Up @@ -45,6 +45,7 @@ public class ObjectResultExecutor

OptionsFormatters = options.Value.OutputFormatters;
RespectBrowserAcceptHeader = options.Value.RespectBrowserAcceptHeader;
ReturnHttpNotAcceptable = options.Value.ReturnHttpNotAcceptable;
Logger = loggerFactory.CreateLogger<ObjectResultExecutor>();
WriterFactory = writerFactory.CreateWriter;
}
Expand All @@ -64,6 +65,11 @@ public class ObjectResultExecutor
/// </summary>
protected bool RespectBrowserAcceptHeader { get; }

/// <summary>
/// Gets the value of <see cref="MvcOptions.ReturnHttpNotAcceptable"/>.
/// </summary>
protected bool ReturnHttpNotAcceptable { get; }

/// <summary>
/// Gets the writer factory delegate.
/// </summary>
Expand Down Expand Up @@ -130,7 +136,7 @@ public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
{
// No formatter supports this.
Logger.NoFormatter(formatterContext);

context.HttpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable;
return TaskCache.CompletedTask;
}
Expand Down Expand Up @@ -175,71 +181,57 @@ public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
throw new ArgumentNullException(nameof(formatters));
}

// Check if any content-type was explicitly set (for example, via ProducesAttribute
// or URL path extension mapping). If yes, then ignore content-negotiation and use this content-type.
if (contentTypes.Count == 1)
var request = formatterContext.HttpContext.Request;
var acceptableMediaTypes = GetAcceptableMediaTypes(contentTypes, request);
var selectFormatterWithoutRegardingAcceptHeader = false;
IOutputFormatter selectedFormatter = null;

if (acceptableMediaTypes.Count == 0)
{
Logger.SkippedContentNegotiation(contentTypes[0]);
// There is either no Accept header value, or it contained */* and we
// are not currently respecting the 'browser accept header'.
Logger.NoAcceptForNegotiation();

return SelectFormatterUsingAnyAcceptableContentType(formatterContext, formatters, contentTypes);
selectFormatterWithoutRegardingAcceptHeader = true;
}

var request = formatterContext.HttpContext.Request;

var mediaTypes = GetMediaTypes(contentTypes, request);
IOutputFormatter selectedFormatter = null;
if (contentTypes.Count == 0)
else
{
// Check if we have enough information to do content-negotiation, otherwise get the first formatter
// which can write the type. Let the formatter choose the Content-Type.
if (!(mediaTypes.Count > 0))
if (contentTypes.Count == 0)
{
Logger.NoAcceptForNegotiation();

return SelectFormatterNotUsingAcceptHeaders(formatterContext, formatters);
// Use whatever formatter can meet the client's request
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
formatterContext,
formatters,
acceptableMediaTypes);
}

//
// Content-Negotiation starts from this point on.
//

// 1. Select based on sorted accept headers.
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
formatterContext,
formatters,
mediaTypes);

// 2. No formatter was found based on Accept header. Fallback to the first formatter which can write
// the type. Let the formatter choose the Content-Type.
if (selectedFormatter == null)
else
{
Logger.NoFormatterFromNegotiation(mediaTypes);
// Verify that a content type from the context is compatible with the client's request
selectedFormatter = SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
formatterContext,
formatters,
acceptableMediaTypes,
contentTypes);
}

// Set this flag to indicate that content-negotiation has failed to let formatters decide
// if they want to write the response or not.
formatterContext.FailedContentNegotiation = true;
if (selectedFormatter == null && !ReturnHttpNotAcceptable)
{
Logger.NoFormatterFromNegotiation(acceptableMediaTypes);

return SelectFormatterNotUsingAcceptHeaders(formatterContext, formatters);
selectFormatterWithoutRegardingAcceptHeader = true;
}
}
else

if (selectFormatterWithoutRegardingAcceptHeader)
{
if (mediaTypes.Count > 0)
if (contentTypes.Count == 0)
{
selectedFormatter = SelectFormatterUsingSortedAcceptHeaders(
selectedFormatter = SelectFormatterNotUsingContentType(
formatterContext,
formatters,
mediaTypes);
formatters);
}

if (selectedFormatter == null)
else
{
// Either there were no acceptHeaders that were present OR
// There were no accept headers which matched OR
// There were acceptHeaders which matched but there was no formatter
// which supported any of them.
// In any of these cases, if the user has specified content types,
// do a last effort to find a formatter which can write any of the user specified content type.
selectedFormatter = SelectFormatterUsingAnyAcceptableContentType(
formatterContext,
formatters,
Expand All @@ -250,7 +242,7 @@ public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
return selectedFormatter;
}

private List<MediaTypeSegmentWithQuality> GetMediaTypes(
private List<MediaTypeSegmentWithQuality> GetAcceptableMediaTypes(
MediaTypeCollection contentTypes,
HttpRequest request)
{
Expand All @@ -264,38 +256,13 @@ public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
result.Clear();
return result;
}

if (!InAcceptableMediaTypes(result[i].MediaType, contentTypes))
{
result.RemoveAt(i);
}
}

result.Sort((left, right) => left.Quality > right.Quality ? -1 : (left.Quality == right.Quality ? 0 : 1));

return result;
}

private static bool InAcceptableMediaTypes(StringSegment mediaType, MediaTypeCollection acceptableMediaTypes)
{
if (acceptableMediaTypes.Count == 0)
{
return true;
}

var parsedMediaType = new MediaType(mediaType);
for (int i = 0; i < acceptableMediaTypes.Count; i++)
{
var acceptableMediaType = new MediaType(acceptableMediaTypes[i]);
if (acceptableMediaType.IsSubsetOf(parsedMediaType))
{
return true;
}
}

return false;
}

/// <summary>
/// Selects the <see cref="IOutputFormatter"/> to write the response. The first formatter which
/// can write the response should be chosen without any consideration for content type.
Expand All @@ -307,7 +274,7 @@ private static bool InAcceptableMediaTypes(StringSegment mediaType, MediaTypeCol
/// <returns>
/// The selected <see cref="IOutputFormatter"/> or <c>null</c> if no formatter can write the response.
/// </returns>
protected virtual IOutputFormatter SelectFormatterNotUsingAcceptHeaders(
protected virtual IOutputFormatter SelectFormatterNotUsingContentType(
OutputFormatterWriteContext formatterContext,
IList<IOutputFormatter> formatters)
{
Expand Down Expand Up @@ -432,6 +399,53 @@ private static bool InAcceptableMediaTypes(StringSegment mediaType, MediaTypeCol

return null;
}

/// <summary>
/// Selects the <see cref="IOutputFormatter"/> to write the response based on the content type values
/// present in <paramref name="sortedAcceptableContentTypes"/> and <paramref name="possibleOutputContentTypes"/>.
/// </summary>
/// <param name="formatterContext">The <see cref="OutputFormatterWriteContext"/>.</param>
/// <param name="formatters">
/// The list of <see cref="IOutputFormatter"/> instances to consider.
/// </param>
/// <param name="sortedAcceptableContentTypes">
/// The ordered content types from the <c>Accept</c> header, sorted by descending q-value.
/// </param>
/// <param name="possibleOutputContentTypes">
/// The ordered content types from <see cref="ObjectResult.ContentTypes"/> in descending priority order.
/// </param>
/// <returns>
/// The selected <see cref="IOutputFormatter"/> or <c>null</c> if no formatter can write the response.
/// </returns>
protected virtual IOutputFormatter SelectFormatterUsingSortedAcceptHeadersAndContentTypes(
OutputFormatterWriteContext formatterContext,
IList<IOutputFormatter> formatters,
IList<MediaTypeSegmentWithQuality> sortedAcceptableContentTypes,
MediaTypeCollection possibleOutputContentTypes)
{
for (var i = 0; i < sortedAcceptableContentTypes.Count; i++)
{
var acceptableContentType = new MediaType(sortedAcceptableContentTypes[i].MediaType);
for (var j = 0; j < possibleOutputContentTypes.Count; j++)
{
var candidateContentType = new MediaType(possibleOutputContentTypes[j]);
if (candidateContentType.IsSubsetOf(acceptableContentType))
{
for (var k = 0; k < formatters.Count; k++)
{
var formatter = formatters[k];
formatterContext.ContentType = new StringSegment(possibleOutputContentTypes[j]);
if (formatter.CanWriteResult(formatterContext))
{
return formatter;
}
}
}
}
}

return null;
}

private void ValidateContentTypes(MediaTypeCollection contentTypes)
{
Expand Down
7 changes: 7 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.Core/MvcOptions.cs
Expand Up @@ -123,6 +123,13 @@ public int MaxModelValidationErrors
/// </summary>
public bool RespectBrowserAcceptHeader { get; set; }

/// <summary>
/// Gets or sets the flag which decides whether an HTTP 406 Not Acceptable response
/// will be returned if no formatter has been selected to format the response.
/// <see langword="false"/> by default.
/// </summary>
public bool ReturnHttpNotAcceptable { get; set; }

/// <summary>
/// Gets a list of <see cref="IValueProviderFactory"/> used by this application.
/// </summary>
Expand Down

This file was deleted.

0 comments on commit 5a3875e

Please sign in to comment.