@@ -583,29 +583,18 @@ protected virtual async Task HandleRequestAsync(
583583 // Normal execution with single graphql request
584584 var userContext = await BuildUserContextAsync ( context , null ) ;
585585 var result = await ExecuteRequestAsync ( context , gqlRequest , context . RequestServices , userContext ) ;
586- HttpStatusCode statusCode = HttpStatusCode . OK ;
587586 // when the request fails validation (this logic does not apply to execution errors)
588587 if ( ! result . Executed )
589588 {
590589 // always return 405 Method Not Allowed when applicable, as this is a transport problem, not really a validation error,
591590 // even though it occurs during validation (because the query text must be parsed to know if the request is a query or a mutation)
592591 if ( result . Errors ? . Any ( e => e is HttpMethodValidationError ) == true )
593592 {
594- statusCode = HttpStatusCode . MethodNotAllowed ;
595- }
596- // otherwise use 4xx error codes when configured to do so
597- else if ( _options . ValidationErrorsReturnBadRequest )
598- {
599- statusCode = HttpStatusCode . BadRequest ;
600- // if all errors being returned prefer the same status code, use that
601- if ( result . Errors ? . Count > 0 && result . Errors [ 0 ] is IHasPreferredStatusCode initialError )
602- {
603- if ( result . Errors . All ( e => e is IHasPreferredStatusCode e2 && e2 . PreferredStatusCode == initialError . PreferredStatusCode ) )
604- statusCode = initialError . PreferredStatusCode ;
605- }
593+ await WriteJsonResponseAsync ( context , HttpStatusCode . MethodNotAllowed , result ) ;
594+ return ;
606595 }
607596 }
608- await WriteJsonResponseAsync ( context , statusCode , result ) ;
597+ await WriteJsonResponseAsync ( context , result ) ;
609598 }
610599
611600 /// <summary>
@@ -750,10 +739,11 @@ protected virtual async Task<ExecutionResult> ExecuteRequestAsync(HttpContext co
750739 ValueTask < IDictionary < string , object ? > ? > IUserContextBuilder . BuildUserContextAsync( HttpContext context , object ? payload )
751740 => BuildUserContextAsync ( context , payload ) ;
752741
742+ private static readonly MediaTypeHeaderValueMs _applicationJsonMediaType = MediaTypeHeaderValueMs . Parse ( CONTENTTYPE_JSON ) ;
753743 private static readonly MediaTypeHeaderValueMs [ ] _validMediaTypes = new [ ]
754744 {
755745 MediaTypeHeaderValueMs . Parse ( CONTENTTYPE_GRAPHQLRESPONSEJSON ) ,
756- MediaTypeHeaderValueMs . Parse ( CONTENTTYPE_JSON ) ,
746+ _applicationJsonMediaType ,
757747 MediaTypeHeaderValueMs . Parse ( CONTENTTYPE_GRAPHQLJSON ) , // deprecated
758748 } ;
759749
@@ -771,62 +761,87 @@ protected virtual async Task<ExecutionResult> ExecuteRequestAsync(HttpContext co
771761 /// For more complex behavior patterns, override
772762 /// <see cref="WriteJsonResponseAsync{TResult}(HttpContext, HttpStatusCode, TResult)"/>.
773763 /// </summary>
774- protected virtual string SelectResponseContentType ( HttpContext context )
764+ protected virtual MediaTypeHeaderValueMs SelectResponseContentType ( HttpContext context )
775765 {
776766 // pull the Accept header, which may contain multiple content types
777767 var acceptHeaders = context . Request . Headers . ContainsKey ( Microsoft . Net . Http . Headers . HeaderNames . Accept )
778768 ? context . Request . GetTypedHeaders ( ) . Accept
779769 : Array . Empty < MediaTypeHeaderValueMs > ( ) ;
780770
781- if ( acceptHeaders . Count > 0 )
771+ if ( acceptHeaders . Count == 1 )
772+ {
773+ var response = IsSupportedMediaType ( acceptHeaders [ 0 ] ) ;
774+ if ( response != null )
775+ return response ;
776+ }
777+ else if ( acceptHeaders . Count > 0 )
782778 {
783779 // enumerate through each content type and see if it matches a supported content type
784780 // give priority to specific types, then to types with wildcards
785781 foreach ( var acceptHeader in acceptHeaders . OrderBy ( x => x . MatchesAllTypes ? 4 : x . MatchesAllSubTypes ? 3 : x . MatchesAllSubTypesWithoutSuffix ? 2 : 1 ) )
786782 {
787- var response = CheckForMatch ( acceptHeader ) ;
783+ var response = IsSupportedMediaType ( acceptHeader ) ;
788784 if ( response != null )
789785 return response ;
790786 }
791787 }
792788
793789 // return the default content type if no match is found, or if there is no 'Accept' header
794- return _options . DefaultResponseContentTypeString ;
790+ return _options . DefaultResponseContentType ;
791+ }
795792
796- string ? CheckForMatch ( MediaTypeHeaderValueMs acceptHeader )
797- {
798- // strip quotes from charset
799- if ( acceptHeader . Charset . Length > 0 && acceptHeader . Charset [ 0 ] == '\" ' && acceptHeader . Charset [ acceptHeader . Charset . Length - 1 ] == '\" ' )
800- {
801- acceptHeader . Charset = acceptHeader . Charset . Substring ( 1 , acceptHeader . Charset . Length - 2 ) ;
802- }
793+ /// <summary>
794+ /// Checks to see if the specified <see cref="MediaTypeHeaderValueMs"/> matches any of the supported content types
795+ /// by this middleware. If a match is found, the matching content type is returned; otherwise, <see langword="null"/>.
796+ /// Prioritizes <see cref="GraphQLHttpMiddlewareOptions.DefaultResponseContentType"/>, then
797+ /// <c>application/graphql-response+json</c>, then <c>application/json</c>.
798+ /// </summary>
799+ private MediaTypeHeaderValueMs ? IsSupportedMediaType ( MediaTypeHeaderValueMs acceptHeader )
800+ => IsSupportedMediaType ( acceptHeader , _options . DefaultResponseContentType , _validMediaTypes ) ;
803801
804- // check if this matches the default content type header
805- if ( IsSubsetOf ( _options . DefaultResponseContentType , acceptHeader ) )
806- return _options . DefaultResponseContentTypeString ;
802+ /// <summary>
803+ /// Checks to see if the specified <see cref="MediaTypeHeaderValueMs"/> matches any of the supported content types
804+ /// by this middleware. If a match is found, the matching content type is returned; otherwise, <see langword="null"/>.
805+ /// Prioritizes <see cref="GraphQLHttpMiddlewareOptions.DefaultResponseContentType"/>, then
806+ /// <c>application/graphql-response+json</c>, then <c>application/json</c>.
807+ /// </summary>
808+ private static MediaTypeHeaderValueMs ? IsSupportedMediaType ( MediaTypeHeaderValueMs acceptHeader , MediaTypeHeaderValueMs preferredContentType , MediaTypeHeaderValueMs [ ] allowedContentTypes )
809+ {
810+ // speeds check in WriteJsonResponseAsync
811+ if ( acceptHeader == preferredContentType )
812+ return preferredContentType ;
807813
808- // if the default content type header does not contain a charset, test with utf-8 as the charset
809- if ( _options . DefaultResponseContentType . Charset . Length == 0 )
810- {
811- var contentType2 = _options . DefaultResponseContentType . Copy ( ) ;
812- contentType2 . Charset = "utf-8" ;
813- if ( IsSubsetOf ( contentType2 , acceptHeader ) )
814- return contentType2 . ToString ( ) ;
815- }
814+ // strip quotes from charset
815+ if ( acceptHeader . Charset . Length > 0 && acceptHeader . Charset [ 0 ] == '\" ' && acceptHeader . Charset [ acceptHeader . Charset . Length - 1 ] == '\" ' )
816+ {
817+ acceptHeader . Charset = acceptHeader . Charset . Substring ( 1 , acceptHeader . Charset . Length - 2 ) ;
818+ }
816819
817- // loop through the other supported media types, attempting to find a match
818- for ( int j = 0 ; j < _validMediaTypes . Length ; j ++ )
819- {
820- var mediaType = _validMediaTypes [ j ] ;
821- if ( IsSubsetOf ( mediaType , acceptHeader ) )
822- // when a match is found, return the match
823- return mediaType . ToString ( ) ;
824- }
820+ // check if this matches the default content type header
821+ if ( IsSubsetOf ( preferredContentType , acceptHeader ) )
822+ return preferredContentType ;
823+
824+ // if the default content type header does not contain a charset, test with utf-8 as the charset
825+ if ( preferredContentType . Charset . Length == 0 )
826+ {
827+ var contentType2 = preferredContentType . Copy ( ) ;
828+ contentType2 . Charset = "utf-8" ;
829+ if ( IsSubsetOf ( contentType2 , acceptHeader ) )
830+ return contentType2 ;
831+ }
825832
826- // no match
827- return null ;
833+ // loop through the other supported media types, attempting to find a match
834+ for ( int j = 0 ; j < allowedContentTypes . Length ; j ++ )
835+ {
836+ var mediaType = allowedContentTypes [ j ] ;
837+ if ( IsSubsetOf ( mediaType , acceptHeader ) )
838+ // when a match is found, return the match
839+ return mediaType ;
828840 }
829841
842+ // no match
843+ return null ;
844+
830845 // --- note: the below functions were copied from ASP.NET Core 2.1 source ---
831846 // see https://github.com/dotnet/aspnetcore/blob/v2.1.33/src/Http/Headers/src/MediaTypeHeaderValue.cs
832847
@@ -940,11 +955,41 @@ static bool MatchesSubtypeSuffix(MediaTypeHeaderValueMs mediaType, MediaTypeHead
940955 }
941956
942957 /// <summary>
943- /// Writes the specified object (usually a GraphQL response represented as an instance of <see cref="ExecutionResult"/>) as JSON to the HTTP response stream.
958+ /// Writes the specified <see cref="ExecutionResult"/> as JSON to the HTTP response stream,
959+ /// selecting the proper content type and status code based on the request Accept header and response.
960+ /// </summary>
961+ protected virtual Task WriteJsonResponseAsync ( HttpContext context , ExecutionResult result )
962+ {
963+ var contentType = SelectResponseContentType ( context ) ;
964+ context . Response . ContentType = contentType == _options . DefaultResponseContentType ? _options . DefaultResponseContentTypeString : contentType . ToString ( ) ;
965+ context . Response . StatusCode = ( int ) HttpStatusCode . OK ;
966+ if ( result . Executed == false )
967+ {
968+ var useBadRequest = _options . ValidationErrorsReturnBadRequest ?? IsSupportedMediaType ( contentType , _applicationJsonMediaType , Array . Empty < MediaTypeHeaderValueMs > ( ) ) == null ;
969+ if ( useBadRequest )
970+ {
971+ context . Response . StatusCode = ( int ) HttpStatusCode . BadRequest ;
972+
973+ // if all errors being returned prefer the same status code, use that
974+ if ( result . Errors ? . Count > 0 && result . Errors [ 0 ] is IHasPreferredStatusCode initialError )
975+ {
976+ if ( result . Errors . All ( e => e is IHasPreferredStatusCode e2 && e2 . PreferredStatusCode == initialError . PreferredStatusCode ) )
977+ context . Response . StatusCode = ( int ) initialError . PreferredStatusCode ;
978+ }
979+ }
980+ }
981+
982+ return _serializer . WriteAsync ( context . Response . Body , result , context . RequestAborted ) ;
983+ }
984+
985+ /// <summary>
986+ /// Writes the specified object (usually a GraphQL response represented as an instance of <see cref="ExecutionResult"/>)
987+ /// as JSON to the HTTP response stream, using the specified status code.
944988 /// </summary>
945989 protected virtual Task WriteJsonResponseAsync < TResult > ( HttpContext context , HttpStatusCode httpStatusCode , TResult result )
946990 {
947- context . Response . ContentType = SelectResponseContentType ( context ) ;
991+ var contentType = SelectResponseContentType ( context ) ;
992+ context . Response . ContentType = contentType == _options . DefaultResponseContentType ? _options . DefaultResponseContentTypeString : contentType . ToString ( ) ;
948993 context . Response . StatusCode = ( int ) httpStatusCode ;
949994
950995 return _serializer . WriteAsync ( context . Response . Body , result , context . RequestAborted ) ;
0 commit comments