Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ignore Header parameters so that charset=utf-8 will not fail to match on a valid Formatter #17

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/Hl7.Fhir.WebApi.AspNetCore/FhirMediaTypeFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
using System.Linq;
using Hl7.Fhir.Serialization;
using Hl7.Fhir.Utility;
using Microsoft.Extensions.Primitives;
using System.Resources;

namespace Hl7.Fhir.WebApi
{
Expand Down Expand Up @@ -120,6 +122,71 @@ protected override bool CanWriteType(Type type)
return false;
}

/// <inheritdoc />
public override bool CanWriteResult(OutputFormatterCanWriteContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

if (SupportedMediaTypes.Count == 0)
{
var message = $"No media types found in '{GetType().FullName}.{nameof(SupportedMediaTypes)}'. Add at least one media type to the list of supported media types.";

throw new InvalidOperationException(message);
}

if (!CanWriteType(context.ObjectType))
{
return false;
}

if (!context.ContentType.HasValue)
{
// If the desired content type is set to null, then the current formatter can write anything
// it wants.
context.ContentType = new StringSegment(SupportedMediaTypes[0]);
return true;
}
else
{
var parsedContentType = new MediaType(context.ContentType);
for (var i = 0; i < SupportedMediaTypes.Count; i++)
{
var supportedMediaType = new MediaType(SupportedMediaTypes[i]);
if (supportedMediaType.HasWildcard)
{
// For supported media types that are wildcard patterns, confirm that the requested
// media type satisfies the wildcard pattern (e.g., if "text/entity+json;v=2" requested
// and formatter supports "text/*+json").
// We only do this when comparing against server-defined content types (e.g., those
// from [Produces] or Response.ContentType), otherwise we'd potentially be reflecting
// back arbitrary Accept header values.
if (context.ContentTypeIsServerDefined
&& parsedContentType.IsSubsetOf(supportedMediaType))
{
return true;
}
}
else
{
// For supported media types that are not wildcard patterns, confirm that this formatter
// supports a more specific media type than requested e.g. OK if "text/*" requested and
// formatter supports "text/plain".
// contentType is typically what we got in an Accept header.
if (supportedMediaType.IsSubsetOfIgnoreParameters(parsedContentType))
{
context.ContentType = new StringSegment(SupportedMediaTypes[i]);
return true;
}
}
}
}

return false;
}

const string x_correlation_id = "X-Correlation-Id";
public override void WriteResponseHeaders(OutputFormatterWriteContext context)
{
Expand Down
63 changes: 63 additions & 0 deletions src/Hl7.Fhir.WebApi.AspNetCore/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
using System.Net;
using System.Net.Http;
using System.Text;
using Microsoft.AspNetCore.Mvc.Formatters;

namespace Hl7.Fhir.WebApi
{
Expand Down Expand Up @@ -185,5 +186,67 @@ public static string ToFhirDateTime(this System.DateTime? me)
}
#endregion

#region << Static Helpers for Header Subset comparison that ignores Header parameters >>
public static bool IsSubsetOfIgnoreParameters(this MediaType supported, MediaType set)
{
return supported.MatchesType(set) &&
supported.MatchesSubtype(set);
}

private static bool MatchesType(this MediaType supported, MediaType set)
{
return set.MatchesAllTypes ||
set.Type.Equals(supported.Type, StringComparison.OrdinalIgnoreCase);
}

private static bool MatchesSubtype(this MediaType supported, MediaType set)
{
if (set.MatchesAllSubTypes)
{
return true;
}

if (set.SubTypeSuffix.HasValue)
{
if (supported.SubTypeSuffix.HasValue)
{
// Both the set and the media type being checked have suffixes, so both parts must match.
return supported.MatchesSubtypeWithoutSuffix(set) && supported.MatchesSubtypeSuffix(set);
}
else
{
// The set has a suffix, but the media type being checked doesn't. We never consider this to match.
return false;
}
}
else
{
// If this subtype or suffix matches the subtype of the set,
// it is considered a subtype.
// Ex: application/json > application/val+json
return supported.MatchesEitherSubtypeOrSuffix(set);
}
}

private static bool MatchesSubtypeWithoutSuffix(this MediaType supported, MediaType set)
{
return set.MatchesAllSubTypesWithoutSuffix ||
set.SubTypeWithoutSuffix.Equals(supported.SubTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase);
}

private static bool MatchesSubtypeSuffix(this MediaType supported, MediaType set)
{
// We don't have support for wildcards on suffixes alone (e.g., "application/entity+*")
// because there's no clear use case for it.
return set.SubTypeSuffix.Equals(supported.SubTypeSuffix, StringComparison.OrdinalIgnoreCase);
}

private static bool MatchesEitherSubtypeOrSuffix(this MediaType supported, MediaType set)
{
return set.SubType.Equals(supported.SubType, StringComparison.OrdinalIgnoreCase) ||
set.SubType.Equals(supported.SubTypeSuffix, StringComparison.OrdinalIgnoreCase);
}

#endregion
}
}
75 changes: 75 additions & 0 deletions src/Test.WebApi.AspNetCore/BasicTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Hl7.Fhir.Validation;
using Hl7.Fhir.WebApi;
using Microsoft.EntityFrameworkCore;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
Expand All @@ -14,6 +15,7 @@
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading;
using Task = System.Threading.Tasks.Task;

Expand Down Expand Up @@ -951,6 +953,79 @@ public async System.Threading.Tasks.Task ReadBinary()

}

[TestMethod]
public async Task RequestAcceptJsonWithHeaderParameter()
{
var app = new UnitTestFhirServerApplication();
var httpClient = app.CreateClient();
var acceptHeader = "application/json+fhir";
var acceptHeaderWithEncoding = "application/json+fhir; charset=utf-8";
var acceptHeaderWithEncodingAndVersion = "application/json+fhir; carset=utf-8; fhirVersion=4.0";
var badAcceptHeader = "application/notjson+fhir";
var acceptXmlHeader = "application/xml+fhir";

httpClient.DefaultRequestHeaders.Add("Accept", acceptHeader);
var raw = await httpClient.GetStringAsync($"{app.Server.BaseAddress}Patient");

try
{
await new FhirJsonParser().ParseAsync<Bundle>(raw);
}
catch (Exception ex)
{
Assert.Fail("Expected Json formatted bundle: " + ex.Message);
}

httpClient.DefaultRequestHeaders.Remove("Accept");
httpClient.DefaultRequestHeaders.Add("Accept", acceptHeaderWithEncoding);
raw = await httpClient.GetStringAsync($"{app.Server.BaseAddress}Patient");
try
{
await new FhirJsonParser().ParseAsync<Bundle>(raw);
}
catch (Exception ex)
{
Assert.Fail("Expected Json formatted bundle: " + ex.Message);
}

httpClient.DefaultRequestHeaders.Remove("Accept");
httpClient.DefaultRequestHeaders.Add("Accept", acceptHeaderWithEncodingAndVersion);
raw = await httpClient.GetStringAsync($"{app.Server.BaseAddress}Patient");
try
{
await new FhirJsonParser().ParseAsync<Bundle>(raw);
}
catch (Exception ex)
{
Assert.Fail("Expected Json formatted bundle: " + ex.Message);
}

httpClient.DefaultRequestHeaders.Remove("Accept");
httpClient.DefaultRequestHeaders.Add("Accept", badAcceptHeader);
raw = await httpClient.GetStringAsync($"{app.Server.BaseAddress}Patient");
try
{
await new FhirXmlParser().ParseAsync<Bundle>(raw);
}
catch (Exception ex)
{
Assert.Fail("Expected Xml formatted bundle: " + ex.Message);
}

httpClient.DefaultRequestHeaders.Remove("Accept");
httpClient.DefaultRequestHeaders.Add("Accept", acceptXmlHeader);
raw = await httpClient.GetStringAsync($"{app.Server.BaseAddress}Patient");
try
{
await new FhirXmlParser().ParseAsync<Bundle>(raw);
}
catch (Exception ex)
{
Assert.Fail("Expected Xml formatted bundle: " + ex.Message);
}

}

private void ClientFhir_OnBeforeRequest(object sender, HttpRequestMessage msg)
{
System.Diagnostics.Trace.WriteLine("---------------------------------------------------");
Expand Down