@@ -583,29 +583,18 @@ protected virtual async Task HandleRequestAsync(
583
583
// Normal execution with single graphql request
584
584
var userContext = await BuildUserContextAsync ( context , null ) ;
585
585
var result = await ExecuteRequestAsync ( context , gqlRequest , context . RequestServices , userContext ) ;
586
- HttpStatusCode statusCode = HttpStatusCode . OK ;
587
586
// when the request fails validation (this logic does not apply to execution errors)
588
587
if ( ! result . Executed )
589
588
{
590
589
// always return 405 Method Not Allowed when applicable, as this is a transport problem, not really a validation error,
591
590
// even though it occurs during validation (because the query text must be parsed to know if the request is a query or a mutation)
592
591
if ( result . Errors ? . Any ( e => e is HttpMethodValidationError ) == true )
593
592
{
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 ;
606
595
}
607
596
}
608
- await WriteJsonResponseAsync ( context , statusCode , result ) ;
597
+ await WriteJsonResponseAsync ( context , result ) ;
609
598
}
610
599
611
600
/// <summary>
@@ -750,10 +739,11 @@ protected virtual async Task<ExecutionResult> ExecuteRequestAsync(HttpContext co
750
739
ValueTask < IDictionary < string , object ? > ? > IUserContextBuilder . BuildUserContextAsync( HttpContext context , object ? payload )
751
740
=> BuildUserContextAsync ( context , payload ) ;
752
741
742
+ private static readonly MediaTypeHeaderValueMs _applicationJsonMediaType = MediaTypeHeaderValueMs . Parse ( CONTENTTYPE_JSON ) ;
753
743
private static readonly MediaTypeHeaderValueMs [ ] _validMediaTypes = new [ ]
754
744
{
755
745
MediaTypeHeaderValueMs . Parse ( CONTENTTYPE_GRAPHQLRESPONSEJSON ) ,
756
- MediaTypeHeaderValueMs . Parse ( CONTENTTYPE_JSON ) ,
746
+ _applicationJsonMediaType ,
757
747
MediaTypeHeaderValueMs . Parse ( CONTENTTYPE_GRAPHQLJSON ) , // deprecated
758
748
} ;
759
749
@@ -771,62 +761,87 @@ protected virtual async Task<ExecutionResult> ExecuteRequestAsync(HttpContext co
771
761
/// For more complex behavior patterns, override
772
762
/// <see cref="WriteJsonResponseAsync{TResult}(HttpContext, HttpStatusCode, TResult)"/>.
773
763
/// </summary>
774
- protected virtual string SelectResponseContentType ( HttpContext context )
764
+ protected virtual MediaTypeHeaderValueMs SelectResponseContentType ( HttpContext context )
775
765
{
776
766
// pull the Accept header, which may contain multiple content types
777
767
var acceptHeaders = context . Request . Headers . ContainsKey ( Microsoft . Net . Http . Headers . HeaderNames . Accept )
778
768
? context . Request . GetTypedHeaders ( ) . Accept
779
769
: Array . Empty < MediaTypeHeaderValueMs > ( ) ;
780
770
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 )
782
778
{
783
779
// enumerate through each content type and see if it matches a supported content type
784
780
// give priority to specific types, then to types with wildcards
785
781
foreach ( var acceptHeader in acceptHeaders . OrderBy ( x => x . MatchesAllTypes ? 4 : x . MatchesAllSubTypes ? 3 : x . MatchesAllSubTypesWithoutSuffix ? 2 : 1 ) )
786
782
{
787
- var response = CheckForMatch ( acceptHeader ) ;
783
+ var response = IsSupportedMediaType ( acceptHeader ) ;
788
784
if ( response != null )
789
785
return response ;
790
786
}
791
787
}
792
788
793
789
// 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
+ }
795
792
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 ) ;
803
801
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 ;
807
813
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
+ }
816
819
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
+ }
825
832
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 ;
828
840
}
829
841
842
+ // no match
843
+ return null ;
844
+
830
845
// --- note: the below functions were copied from ASP.NET Core 2.1 source ---
831
846
// see https://github.com/dotnet/aspnetcore/blob/v2.1.33/src/Http/Headers/src/MediaTypeHeaderValue.cs
832
847
@@ -940,11 +955,41 @@ static bool MatchesSubtypeSuffix(MediaTypeHeaderValueMs mediaType, MediaTypeHead
940
955
}
941
956
942
957
/// <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.
944
988
/// </summary>
945
989
protected virtual Task WriteJsonResponseAsync < TResult > ( HttpContext context , HttpStatusCode httpStatusCode , TResult result )
946
990
{
947
- context . Response . ContentType = SelectResponseContentType ( context ) ;
991
+ var contentType = SelectResponseContentType ( context ) ;
992
+ context . Response . ContentType = contentType == _options . DefaultResponseContentType ? _options . DefaultResponseContentTypeString : contentType . ToString ( ) ;
948
993
context . Response . StatusCode = ( int ) httpStatusCode ;
949
994
950
995
return _serializer . WriteAsync ( context . Response . Body , result , context . RequestAborted ) ;
0 commit comments