diff --git a/README.md b/README.md index c838bcc3..a68f3b89 100644 --- a/README.md +++ b/README.md @@ -537,6 +537,7 @@ methods allowing for different options for each configured endpoint. | `AuthorizationRequired` | Requires `HttpContext.User` to represent an authenticated user. | False | | `AuthorizedPolicy` | If set, requires `HttpContext.User` to pass authorization of the specified policy. | | | `AuthorizedRoles` | If set, requires `HttpContext.User` to be a member of any one of a list of roles. | | +| `DefaultResponseContentType` | Sets the default response content type used within responses. | `application/graphql-response+json; charset=utf-8` | | `EnableBatchedRequests` | Enables handling of batched GraphQL requests for POST requests when formatted as JSON. | True | | `ExecuteBatchedRequestsInParallel` | Enables parallel execution of batched GraphQL requests. | True | | `HandleGet` | Enables handling of GET requests. | True | @@ -672,7 +673,7 @@ A list of methods are as follows: | `HandleWebSocketSubProtocolNotSupportedAsync` | Writes a '400 Invalid WebSocket sub-protocol.' message to the output. | Below is a sample of custom middleware to change the response content type to `application/json`, -rather than the default of `application/graphql-response+json`: +regardless of the value of the HTTP 'Accept' header or default value set in the options: ```csharp class MyMiddleware : GraphQLHttpMiddleware diff --git a/docs/migration/migration7.md b/docs/migration/migration7.md index cb431b23..2e061e3f 100644 --- a/docs/migration/migration7.md +++ b/docs/migration/migration7.md @@ -6,7 +6,7 @@ - Configuration simplified to a single line of code - Single middleware to support GET, POST and WebSocket connections (configurable) -- Media type of 'application/graphql-response+json' is accepted and returned as recommended by the draft spec (configurable via virtual method) +- Media type of 'application/graphql-response+json' is returned as recommended by the draft spec (configurable) - Batched requests will execute in parallel within separate service scopes (configurable) - Authorization rules can be set on endpoints, regardless of schema configuration - Mutation requests are disallowed over GET connections, as required by the spec @@ -188,27 +188,17 @@ app.UseGraphQL("/graphqlsubscription", o => {
To retain prior media type of `application/json`

```csharp -class MyMiddleware : GraphQLHttpMiddleware - where TSchema : ISchema -{ - public MyMiddleware( - RequestDelegate next, - IGraphQLTextSerializer serializer, - IDocumentExecuter documentExecuter, - IServiceScopeFactory serviceScopeFactory, - GraphQLHttpMiddlewareOptions options, - IHostApplicationLifetime hostApplicationLifetime) - : base(next, serializer, documentExecuter, serviceScopeFactory, options, hostApplicationLifetime) - { - } +// with no charset specified +app.UseGraphQL("/graphql", o => o.DefaultResponseContentType = new("application/json")); - protected override string SelectResponseContentType(HttpContext context) - => "application/json"; -} - -app.UseGraphQL>("/graphql", new GraphQLHttpMiddlewareOptions()); +// with utf-8 charset specified +app.UseGraphQL("/graphql", o => o.DefaultResponseContentType = new("application/json") { Charset = "utf-8" }); ``` +Note that if a request is received with a specific supported media type such as `application/graphql-response+json`, +then the supported media type will be returned rather than the default. Override the `SelectResponseContentType` +method within the middleware for more precise control of the Content-Type header in the response. +

If you had code within the `RequestExecutedAsync` protected method

diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs index d1267fe9..c1d95ccc 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddleware.cs @@ -1,5 +1,8 @@ #pragma warning disable CA1716 // Identifiers should not match keywords +using Microsoft.Extensions.Primitives; +using MediaTypeHeaderValueMs = Microsoft.Net.Http.Headers.MediaTypeHeaderValue; + namespace GraphQL.Server.Transports.AspNetCore; /// @@ -51,10 +54,12 @@ public class GraphQLHttpMiddleware : IUserContextBuilder private const string VARIABLES_KEY = "variables"; private const string EXTENSIONS_KEY = "extensions"; private const string OPERATION_NAME_KEY = "operationName"; - private const string MEDIATYPE_GRAPHQLJSON = "application/graphql+json"; + private const string MEDIATYPE_GRAPHQLJSON = "application/graphql+json"; // deprecated private const string MEDIATYPE_JSON = "application/json"; private const string MEDIATYPE_GRAPHQL = "application/graphql"; - internal const string CONTENTTYPE_GRAPHQLJSON = "application/graphql-response+json; charset=utf-8"; + internal const string CONTENTTYPE_JSON = "application/json; charset=utf-8"; + internal const string CONTENTTYPE_GRAPHQLJSON = "application/graphql+json; charset=utf-8"; // deprecated + internal const string CONTENTTYPE_GRAPHQLRESPONSEJSON = "application/graphql-response+json; charset=utf-8"; ///

/// Initializes a new instance. @@ -195,7 +200,7 @@ public virtual async Task InvokeAsync(HttpContext context) switch (mediaType?.ToLowerInvariant()) { - case MEDIATYPE_GRAPHQLJSON: + case MEDIATYPE_GRAPHQLJSON: // deprecated case MEDIATYPE_JSON: IList? deserializationResult; try @@ -444,17 +449,192 @@ protected virtual async Task ExecuteRequestAsync(HttpContext co ValueTask?> IUserContextBuilder.BuildUserContextAsync(HttpContext context, object? payload) => BuildUserContextAsync(context, payload); + private static readonly MediaTypeHeaderValueMs[] _validMediaTypes = new[] + { + MediaTypeHeaderValueMs.Parse(CONTENTTYPE_GRAPHQLRESPONSEJSON), + MediaTypeHeaderValueMs.Parse(CONTENTTYPE_JSON), + MediaTypeHeaderValueMs.Parse(CONTENTTYPE_GRAPHQLJSON), // deprecated + }; + /// /// Selects a response content type string based on the . - /// Defaults to . Override this value for compatibility - /// with non-conforming GraphQL clients. + /// The default implementation attempts to match the content-type requested by the + /// client through the 'Accept' HTTP header to the default content type specified + /// within . + /// If matched, the specified content-type is returned; if not, supported + /// content-types are tested ("application/json", "application/graphql+json", and + /// "application/graphql-response+json") to see if they match the 'Accept' header. ///

/// Note that by default, the response will be written as UTF-8 encoded JSON, regardless - /// of the content-type value here. For more complex behavior patterns, override + /// of the content-type value here, and this method's default implementation assumes as much. + /// For more complex behavior patterns, override /// . ///
protected virtual string SelectResponseContentType(HttpContext context) - => _options.ResponseContentType; + { + // pull the Accept header, which may contain multiple content types + var acceptHeaders = context.Request.GetTypedHeaders().Accept; + + if (acceptHeaders != null) + { + // enumerate through each content type and see if it matches a supported content type + // give priority to specific types, then to types with wildcards + foreach (var acceptHeader in acceptHeaders.OrderBy(x => x.MatchesAllTypes ? 4 : x.MatchesAllSubTypes ? 3 : x.MatchesAllSubTypesWithoutSuffix ? 2 : 1)) + { + var response = CheckForMatch(acceptHeader); + if (response != null) + return response; + } + } + + // return the default content type if no match is found, or if there is no 'Accept' header + return _options.DefaultResponseContentType.ToString(); + + string? CheckForMatch(MediaTypeHeaderValueMs acceptHeader) + { + // strip quotes from charset + if (acceptHeader.Charset.Length > 0 && acceptHeader.Charset[0] == '\"' && acceptHeader.Charset[acceptHeader.Charset.Length - 1] == '\"') + { + acceptHeader.Charset = acceptHeader.Charset.Substring(1, acceptHeader.Charset.Length - 2); + } + + // check if this matches the default content type header + if (IsSubsetOf(_options.DefaultResponseContentType, acceptHeader)) + return _options.DefaultResponseContentType.ToString(); + + // if the default content type header does not contain a charset, test with utf-8 as the charset + if (_options.DefaultResponseContentType.Charset.Length == 0) + { + var contentType2 = _options.DefaultResponseContentType.Copy(); + contentType2.Charset = "utf-8"; + if (IsSubsetOf(contentType2, acceptHeader)) + return contentType2.ToString(); + } + + // loop through the other supported media types, attempting to find a match + for (int j = 0; j < _validMediaTypes.Length; j++) + { + var mediaType = _validMediaTypes[j]; + if (IsSubsetOf(mediaType, acceptHeader)) + // when a match is found, return the match + return mediaType.ToString(); + } + + // no match + return null; + } + + // --- note: the below functions were copied from ASP.NET Core 2.1 source --- + // see https://github.com/dotnet/aspnetcore/blob/v2.1.33/src/Http/Headers/src/MediaTypeHeaderValue.cs + + // The ASP.NET Core 6.0 source contains logic that is not suitable -- it will consider + // "application/graphql-response+json" to match an 'Accept' header of "application/json", + // which can break client applications. + + /* + * Copyright (c) .NET Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use + * these files except in compliance with the License. You may obtain a copy of the + * License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed + * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + * CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + */ + + static bool IsSubsetOf(MediaTypeHeaderValueMs mediaType, MediaTypeHeaderValueMs otherMediaType) + { + // "text/plain" is a subset of "text/plain", "text/*" and "*/*". "*/*" is a subset only of "*/*". + return MatchesType(mediaType, otherMediaType) && + MatchesSubtype(mediaType, otherMediaType) && + MatchesParameters(mediaType, otherMediaType); + } + + static bool MatchesType(MediaTypeHeaderValueMs mediaType, MediaTypeHeaderValueMs set) + { + return set.MatchesAllTypes || + set.Type.Equals(mediaType.Type, StringComparison.OrdinalIgnoreCase); + } + + static bool MatchesSubtype(MediaTypeHeaderValueMs mediaType, MediaTypeHeaderValueMs set) + { + if (set.MatchesAllSubTypes) + { + return true; + } + if (set.Suffix.HasValue) + { + if (mediaType.Suffix.HasValue) + { + return MatchesSubtypeWithoutSuffix(mediaType, set) && MatchesSubtypeSuffix(mediaType, set); + } + else + { + return false; + } + } + else + { + return set.SubType.Equals(mediaType.SubType, StringComparison.OrdinalIgnoreCase); + } + } + + static bool MatchesSubtypeWithoutSuffix(MediaTypeHeaderValueMs mediaType, MediaTypeHeaderValueMs set) + { + return set.MatchesAllSubTypesWithoutSuffix || + set.SubTypeWithoutSuffix.Equals(mediaType.SubTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase); + } + + static bool MatchesParameters(MediaTypeHeaderValueMs mediaType, MediaTypeHeaderValueMs set) + { + if (set.Parameters.Count != 0) + { + // Make sure all parameters in the potential superset are included locally. Fine to have additional + // parameters locally; they make this one more specific. + foreach (var parameter in set.Parameters) + { + if (parameter.Name.Equals("*", StringComparison.OrdinalIgnoreCase)) + { + // A parameter named "*" has no effect on media type matching, as it is only used as an indication + // that the entire media type string should be treated as a wildcard. + continue; + } + + if (parameter.Name.Equals("q", StringComparison.OrdinalIgnoreCase)) + { + // "q" and later parameters are not involved in media type matching. Quoting the RFC: The first + // "q" parameter (if any) separates the media-range parameter(s) from the accept-params. + break; + } + + var localParameter = Microsoft.Net.Http.Headers.NameValueHeaderValue.Find(mediaType.Parameters, parameter.Name); + if (localParameter == null) + { + // Not found. + return false; + } + + if (!StringSegment.Equals(parameter.Value, localParameter.Value, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + } + return true; + } + + static bool MatchesSubtypeSuffix(MediaTypeHeaderValueMs mediaType, MediaTypeHeaderValueMs set) + // We don't have support for wildcards on suffixes alone (e.g., "application/entity+*") + // because there's no clear use case for it. + => set.Suffix.Equals(mediaType.Suffix, StringComparison.OrdinalIgnoreCase); + + // --- end of ASP.NET Core 2.1 copied functions --- + } /// /// Writes the specified object (usually a GraphQL response represented as an instance of ) as JSON to the HTTP response stream. diff --git a/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs index b944cacc..56c8bb6d 100644 --- a/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs +++ b/src/Transports.AspNetCore/GraphQLHttpMiddlewareOptions.cs @@ -1,3 +1,5 @@ +using MediaTypeHeaderValueMs = Microsoft.Net.Http.Headers.MediaTypeHeaderValue; + namespace GraphQL.Server.Transports.AspNetCore; /// @@ -95,7 +97,8 @@ public class GraphQLHttpMiddlewareOptions : IAuthorizationOptions public GraphQLWebSocketOptions WebSockets { get; set; } = new(); /// - /// The Content-Type to use for GraphQL responses + /// The Content-Type to use for GraphQL responses, if it matches the 'Accept' + /// HTTP request header. Defaults to "application/graphql-response+json; charset=utf-8". /// - public string ResponseContentType { get; set; } = GraphQLHttpMiddleware.CONTENTTYPE_GRAPHQLJSON; + public MediaTypeHeaderValueMs DefaultResponseContentType { get; set; } = MediaTypeHeaderValueMs.Parse(GraphQLHttpMiddleware.CONTENTTYPE_GRAPHQLRESPONSEJSON); } diff --git a/tests/ApiApprovalTests/net5+netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/net5+netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt index 7cc768c2..8ebac688 100644 --- a/tests/ApiApprovalTests/net5+netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/net5+netcoreapp31/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -101,6 +101,7 @@ namespace GraphQL.Server.Transports.AspNetCore public bool AuthorizationRequired { get; set; } public string? AuthorizedPolicy { get; set; } public System.Collections.Generic.List AuthorizedRoles { get; set; } + public Microsoft.Net.Http.Headers.MediaTypeHeaderValue DefaultResponseContentType { get; set; } public bool EnableBatchedRequests { get; set; } public bool ExecuteBatchedRequestsInParallel { get; set; } public bool HandleGet { get; set; } @@ -109,7 +110,6 @@ namespace GraphQL.Server.Transports.AspNetCore public bool ReadExtensionsFromQueryString { get; set; } public bool ReadQueryStringOnPost { get; set; } public bool ReadVariablesFromQueryString { get; set; } - public string ResponseContentType { get; set; } public bool ValidationErrorsReturnBadRequest { get; set; } public GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWebSocketOptions WebSockets { get; set; } } diff --git a/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt b/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt index 27749381..b23ee6a0 100644 --- a/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt +++ b/tests/ApiApprovalTests/netcoreapp21+netstandard20/GraphQL.Server.Transports.AspNetCore.approved.txt @@ -108,6 +108,7 @@ namespace GraphQL.Server.Transports.AspNetCore public bool AuthorizationRequired { get; set; } public string? AuthorizedPolicy { get; set; } public System.Collections.Generic.List AuthorizedRoles { get; set; } + public Microsoft.Net.Http.Headers.MediaTypeHeaderValue DefaultResponseContentType { get; set; } public bool EnableBatchedRequests { get; set; } public bool ExecuteBatchedRequestsInParallel { get; set; } public bool HandleGet { get; set; } @@ -116,7 +117,6 @@ namespace GraphQL.Server.Transports.AspNetCore public bool ReadExtensionsFromQueryString { get; set; } public bool ReadQueryStringOnPost { get; set; } public bool ReadVariablesFromQueryString { get; set; } - public string ResponseContentType { get; set; } public bool ValidationErrorsReturnBadRequest { get; set; } public GraphQL.Server.Transports.AspNetCore.WebSockets.GraphQLWebSocketOptions WebSockets { get; set; } } diff --git a/tests/Transports.AspNetCore.Tests/Middleware/GetTests.cs b/tests/Transports.AspNetCore.Tests/Middleware/GetTests.cs index 1d1f4199..9d1d9b6d 100644 --- a/tests/Transports.AspNetCore.Tests/Middleware/GetTests.cs +++ b/tests/Transports.AspNetCore.Tests/Middleware/GetTests.cs @@ -65,6 +65,103 @@ public async Task BasicTest() await response.ShouldBeAsync(@"{""data"":{""count"":0}}"); } + [Theory] + [InlineData(null, "application/graphql+json", "application/graphql+json; charset=utf-8")] + [InlineData(null, "application/json", "application/json; charset=utf-8")] + [InlineData(null, "application/json; charset=utf-8", "application/json; charset=utf-8")] + [InlineData(null, "application/json; charset=UTF-8", "application/json; charset=utf-8")] + [InlineData(null, "APPLICATION/JSON", "application/json; charset=utf-8")] + [InlineData(null, "APPLICATION/JSON; CHARSET=\"UTF-8\" ", "application/json; charset=utf-8")] + [InlineData(null, "*/*; CHARSET=\"UTF-8\" ", "application/graphql-response+json; charset=utf-8")] + [InlineData(null, "application/*; charset=utf-8", "application/graphql-response+json; charset=utf-8")] + [InlineData(null, "application/*+json; charset=utf-8", "application/graphql-response+json; charset=utf-8")] + [InlineData(null, "application/pdf", "application/graphql-response+json; charset=utf-8")] + [InlineData(null, "application/json; charset=utf-7", "application/graphql-response+json; charset=utf-8")] + [InlineData("application/graphql-response+json; charset=utf-8", "application/graphql+json", "application/graphql+json; charset=utf-8")] + [InlineData("application/graphql-response+json; charset=utf-8", "application/json", "application/json; charset=utf-8")] + [InlineData("application/graphql-response+json; charset=utf-8", "application/json; charset=utf-8", "application/json; charset=utf-8")] + [InlineData("application/graphql-response+json; charset=utf-8", "application/json; charset=UTF-8", "application/json; charset=utf-8")] + [InlineData("application/graphql-response+json; charset=utf-8", "APPLICATION/JSON", "application/json; charset=utf-8")] + [InlineData("application/graphql-response+json; charset=utf-8", "APPLICATION/JSON; CHARSET=\"UTF-8\" ", "application/json; charset=utf-8")] + [InlineData("application/graphql-response+json; charset=utf-8", "*/*; CHARSET=\"UTF-8\" ", "application/graphql-response+json; charset=utf-8")] + [InlineData("application/graphql-response+json; charset=utf-8", "application/*; charset=utf-8", "application/graphql-response+json; charset=utf-8")] + [InlineData("application/graphql-response+json; charset=utf-8", "application/*+json; charset=utf-8", "application/graphql-response+json; charset=utf-8")] + [InlineData("application/graphql-response+json; charset=utf-8", "application/pdf", "application/graphql-response+json; charset=utf-8")] + [InlineData("application/graphql-response+json; charset=utf-8", "application/json; charset=utf-7", "application/graphql-response+json; charset=utf-8")] + [InlineData("application/graphql+json; charset=utf-8", "application/graphql+json", "application/graphql+json; charset=utf-8")] + [InlineData("application/graphql+json; charset=utf-8", "application/json", "application/json; charset=utf-8")] + [InlineData("application/graphql+json; charset=utf-8", "application/json; charset=utf-8", "application/json; charset=utf-8")] + [InlineData("application/graphql+json; charset=utf-8", "application/json; charset=UTF-8", "application/json; charset=utf-8")] + [InlineData("application/graphql+json; charset=utf-8", "APPLICATION/JSON", "application/json; charset=utf-8")] + [InlineData("application/graphql+json; charset=utf-8", "APPLICATION/JSON; CHARSET=\"UTF-8\" ", "application/json; charset=utf-8")] + [InlineData("application/graphql+json; charset=utf-8", "*/*; CHARSET=\"UTF-8\" ", "application/graphql+json; charset=utf-8")] + [InlineData("application/graphql+json; charset=utf-8", "application/*; charset=utf-8", "application/graphql+json; charset=utf-8")] + [InlineData("application/graphql+json; charset=utf-8", "application/*+json; charset=utf-8", "application/graphql+json; charset=utf-8")] + [InlineData("application/graphql+json; charset=utf-8", "application/pdf", "application/graphql+json; charset=utf-8")] + [InlineData("application/graphql+json; charset=utf-8", "application/json; charset=utf-7", "application/graphql+json; charset=utf-8")] + [InlineData("application/json; charset=utf-8", "application/graphql+json", "application/graphql+json; charset=utf-8")] + [InlineData("application/json; charset=utf-8", "application/json", "application/json; charset=utf-8")] + [InlineData("application/json; charset=utf-8", "application/json; charset=utf-8", "application/json; charset=utf-8")] + [InlineData("application/json; charset=utf-8", "application/json; charset=UTF-8", "application/json; charset=utf-8")] + [InlineData("application/json; charset=utf-8", "APPLICATION/JSON", "application/json; charset=utf-8")] + [InlineData("application/json; charset=utf-8", "APPLICATION/JSON; CHARSET=\"UTF-8\" ", "application/json; charset=utf-8")] + [InlineData("application/json; charset=utf-8", "*/*; CHARSET=\"UTF-8\" ", "application/json; charset=utf-8")] + [InlineData("application/json; charset=utf-8", "application/*; charset=utf-8", "application/json; charset=utf-8")] + [InlineData("application/json; charset=utf-8", "application/*+json; charset=utf-8", "application/graphql-response+json; charset=utf-8")] + [InlineData("application/json; charset=utf-8", "application/pdf", "application/json; charset=utf-8")] + [InlineData("application/json; charset=utf-8", "application/json; charset=utf-7", "application/json; charset=utf-8")] + [InlineData("application/json", "application/graphql+json", "application/graphql+json; charset=utf-8")] + [InlineData("application/json", "application/json", "application/json")] + [InlineData("application/json", "application/json; charset=utf-8", "application/json; charset=utf-8")] + [InlineData("application/json", "application/json; charset=UTF-8", "application/json; charset=utf-8")] + [InlineData("application/json", "APPLICATION/JSON", "application/json")] + [InlineData("application/json", "APPLICATION/JSON; CHARSET=\"UTF-8\" ", "application/json; charset=utf-8")] + [InlineData("application/json", "*/*; CHARSET=\"UTF-8\" ", "application/json; charset=utf-8")] + [InlineData("application/json", "application/*; charset=utf-8", "application/json; charset=utf-8")] + [InlineData("application/json", "application/*+json; charset=utf-8", "application/graphql-response+json; charset=utf-8")] + [InlineData("application/json", "application/pdf", "application/json")] + [InlineData("application/json", "application/json; charset=utf-7", "application/json")] + [InlineData(null, "*/*, application/graphql-response+json", "application/graphql-response+json; charset=utf-8")] + [InlineData(null, "*/*, application/graphql+json", "application/graphql+json; charset=utf-8")] + [InlineData(null, "*/*, application/json", "application/json; charset=utf-8")] + [InlineData(null, "*/*, application/pdf", "application/graphql-response+json; charset=utf-8")] + [InlineData(null, "application/*, application/graphql-response+json", "application/graphql-response+json; charset=utf-8")] + [InlineData(null, "application/*, application/graphql+json", "application/graphql+json; charset=utf-8")] + [InlineData(null, "application/*, application/json", "application/json; charset=utf-8")] + [InlineData(null, "application/*, application/pdf", "application/graphql-response+json; charset=utf-8")] + [InlineData(null, "application/*+json, application/graphql-response+json", "application/graphql-response+json; charset=utf-8")] + [InlineData(null, "application/*+json, application/graphql+json", "application/graphql+json; charset=utf-8")] + [InlineData(null, "application/*+json, application/json", "application/json; charset=utf-8")] + [InlineData(null, "application/*+json, application/pdf", "application/graphql-response+json; charset=utf-8")] + [InlineData(null, "application/graphql+json, application/json", "application/graphql+json; charset=utf-8")] + [InlineData("application/json", "*/*, application/graphql-response+json", "application/graphql-response+json; charset=utf-8")] + [InlineData("application/json", "*/*, application/graphql+json", "application/graphql+json; charset=utf-8")] + [InlineData("application/json", "*/*, application/json", "application/json")] + [InlineData("application/json", "*/*, application/pdf", "application/json")] + [InlineData("application/json", "application/*, application/graphql-response+json", "application/graphql-response+json; charset=utf-8")] + [InlineData("application/json", "application/*, application/graphql+json", "application/graphql+json; charset=utf-8")] + [InlineData("application/json", "application/*, application/json", "application/json")] + [InlineData("application/json", "application/*, application/pdf", "application/json")] + [InlineData("application/json", "application/*+json, application/graphql-response+json", "application/graphql-response+json; charset=utf-8")] + [InlineData("application/json", "application/*+json, application/graphql+json", "application/graphql+json; charset=utf-8")] + [InlineData("application/json", "application/*+json, application/json", "application/json")] + [InlineData("application/json", "application/*+json, application/pdf", "application/graphql-response+json; charset=utf-8")] + [InlineData("application/json", "application/graphql+json, application/json", "application/graphql+json; charset=utf-8")] + public async Task AcceptHeaderHonored(string? defaultMediaType, string mediaType, string expected) + { + if (defaultMediaType != null) + { + _options.DefaultResponseContentType = Microsoft.Net.Http.Headers.MediaTypeHeaderValue.Parse(defaultMediaType); + } + var client = _server.CreateClient(); + using var request = new HttpRequestMessage(HttpMethod.Get, "/graphql?query={count}"); + request.Headers.Add("Accept", mediaType); + using var response = await client.SendAsync(request); + var contentType = response.Content.Headers.ContentType?.ToString(); + contentType.ShouldBe(expected); + (await response.Content.ReadAsStringAsync()).ShouldBe(@"{""data"":{""count"":0}}"); + } + [Fact] public async Task NoUseWebSockets() { diff --git a/tests/Transports.AspNetCore.Tests/ShouldlyExtensions.cs b/tests/Transports.AspNetCore.Tests/ShouldlyExtensions.cs index 63ad65c1..3ec2b915 100644 --- a/tests/Transports.AspNetCore.Tests/ShouldlyExtensions.cs +++ b/tests/Transports.AspNetCore.Tests/ShouldlyExtensions.cs @@ -13,8 +13,10 @@ public static Task ShouldBeAsync(this HttpResponseMessage message, string expect public static async Task ShouldBeAsync(this HttpResponseMessage message, HttpStatusCode httpStatusCode, string expectedResponse) { message.StatusCode.ShouldBe(httpStatusCode); - message.Content.Headers.ContentType?.MediaType.ShouldBe("application/graphql-response+json"); - message.Content.Headers.ContentType?.CharSet.ShouldBe("utf-8"); + var contentType = message.Content.Headers.ContentType; + contentType.ShouldNotBeNull(); + contentType.MediaType.ShouldBe("application/graphql-response+json"); + contentType.CharSet.ShouldBe("utf-8"); var actualResponse = await message.Content.ReadAsStringAsync(); actualResponse.ShouldBe(expectedResponse); }