diff --git a/src/Hl7.Fhir.WebApi.AspNetCore/FhirMediaTypeFormatter.cs b/src/Hl7.Fhir.WebApi.AspNetCore/FhirMediaTypeFormatter.cs index 22dd2a0c..ae656029 100644 --- a/src/Hl7.Fhir.WebApi.AspNetCore/FhirMediaTypeFormatter.cs +++ b/src/Hl7.Fhir.WebApi.AspNetCore/FhirMediaTypeFormatter.cs @@ -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 { @@ -120,6 +122,71 @@ protected override bool CanWriteType(Type type) return false; } + /// + 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) { diff --git a/src/Hl7.Fhir.WebApi.AspNetCore/Utility.cs b/src/Hl7.Fhir.WebApi.AspNetCore/Utility.cs index 57b6d561..735a5f17 100644 --- a/src/Hl7.Fhir.WebApi.AspNetCore/Utility.cs +++ b/src/Hl7.Fhir.WebApi.AspNetCore/Utility.cs @@ -17,6 +17,7 @@ using System.Net; using System.Net.Http; using System.Text; +using Microsoft.AspNetCore.Mvc.Formatters; namespace Hl7.Fhir.WebApi { @@ -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 } } diff --git a/src/Test.WebApi.AspNetCore/BasicTests.cs b/src/Test.WebApi.AspNetCore/BasicTests.cs index 7c22731d..fde79ca6 100644 --- a/src/Test.WebApi.AspNetCore/BasicTests.cs +++ b/src/Test.WebApi.AspNetCore/BasicTests.cs @@ -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; @@ -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; @@ -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(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(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(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(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(raw); + } + catch (Exception ex) + { + Assert.Fail("Expected Xml formatted bundle: " + ex.Message); + } + + } + private void ClientFhir_OnBeforeRequest(object sender, HttpRequestMessage msg) { System.Diagnostics.Trace.WriteLine("---------------------------------------------------");