@@ -233,6 +233,74 @@ private static ReadOnlySpan<char> TrimWhiteSpace(ReadOnlySpan<char> value)
233233
234234 return value [ start ..( end + 1 ) ] ;
235235 }
236+
237+ /// <summary>
238+ /// Extracts the media type and charset from the raw Content-Type header
239+ /// without allocating a <see cref="MediaTypeHeaderValue"/>.
240+ /// </summary>
241+ private bool TryGetRawMediaTypeAndCharSet (
242+ out ReadOnlySpan < char > mediaType ,
243+ out string ? charSet )
244+ {
245+ if ( ! _message . Content . Headers . NonValidated . TryGetValues ( ContentTypeHeaderName , out var values ) )
246+ {
247+ mediaType = default ;
248+ charSet = null ;
249+ return false ;
250+ }
251+
252+ var enumerator = values . GetEnumerator ( ) ;
253+ if ( ! enumerator . MoveNext ( ) )
254+ {
255+ mediaType = default ;
256+ charSet = null ;
257+ return false ;
258+ }
259+
260+ var rawValue = enumerator . Current . AsSpan ( ) ;
261+
262+ // Some handlers may emit media type and charset as separate values.
263+ if ( enumerator . MoveNext ( ) )
264+ {
265+ mediaType = NormalizeMediaType ( rawValue ) ;
266+ var charsetValue = enumerator . Current . AsSpan ( ) ;
267+ charSet = IsUtf8 ( charsetValue ) ? Utf8 : charsetValue . Trim ( ) . ToString ( ) ;
268+ return true ;
269+ }
270+
271+ // Single header value — split on ';' to separate media type from parameters.
272+ var semicolonIndex = rawValue . IndexOf ( ';' ) ;
273+ if ( semicolonIndex < 0 )
274+ {
275+ mediaType = TrimWhiteSpace ( rawValue ) ;
276+ charSet = null ;
277+ return true ;
278+ }
279+
280+ mediaType = TrimWhiteSpace ( rawValue [ ..semicolonIndex ] ) ;
281+ var parameters = rawValue [ ( semicolonIndex + 1 ) ..] ;
282+
283+ // Extract charset from parameters (e.g., " charset=utf-8").
284+ var charsetIndex = parameters . IndexOf ( CharsetPrefix , StringComparison . OrdinalIgnoreCase ) ;
285+ if ( charsetIndex >= 0 )
286+ {
287+ var charsetSpan = TrimWhiteSpace ( parameters [ ( charsetIndex + CharsetPrefix . Length ) ..] ) ;
288+
289+ // Strip quotes if present.
290+ if ( charsetSpan . Length > 1 && charsetSpan [ 0 ] == '"' && charsetSpan [ ^ 1 ] == '"' )
291+ {
292+ charsetSpan = charsetSpan [ 1 ..^ 1 ] ;
293+ }
294+
295+ charSet = charsetSpan . Equals ( Utf8 , StringComparison . OrdinalIgnoreCase ) ? Utf8 : charsetSpan . ToString ( ) ;
296+ }
297+ else
298+ {
299+ charSet = null ;
300+ }
301+
302+ return true ;
303+ }
236304#endif
237305
238306 /// <summary>
@@ -258,6 +326,32 @@ private static ReadOnlySpan<char> TrimWhiteSpace(ReadOnlySpan<char> value)
258326 /// to read the <see cref="SourceResultDocument"/> from the underlying <see cref="HttpResponseMessage"/>.
259327 /// </returns>
260328 public ValueTask < SourceResultDocument > ReadAsResultAsync ( CancellationToken cancellationToken = default )
329+ {
330+ if ( ! TryGetRawMediaTypeAndCharSet ( out var mediaType , out var charSet ) )
331+ {
332+ _message . EnsureSuccessStatusCode ( ) ;
333+ throw new InvalidOperationException ( "Received a successful response with an unexpected content type." ) ;
334+ }
335+
336+ // The server supports the newer graphql-response+json media type, and users are free
337+ // to use status codes.
338+ if ( mediaType . Equals ( ContentType . GraphQL , StringComparison . OrdinalIgnoreCase ) )
339+ {
340+ return ReadAsResultInternalAsync ( charSet , cancellationToken ) ;
341+ }
342+
343+ // The server supports the older application/json media type, and the status code
344+ // is expected to be a 2xx for a valid GraphQL response.
345+ if ( mediaType . Equals ( ContentType . Json , StringComparison . OrdinalIgnoreCase ) )
346+ {
347+ _message . EnsureSuccessStatusCode ( ) ;
348+ return ReadAsResultInternalAsync ( charSet , cancellationToken ) ;
349+ }
350+
351+ _message . EnsureSuccessStatusCode ( ) ;
352+
353+ throw new InvalidOperationException ( "Received a successful response with an unexpected content type." ) ;
354+ }
261355#else
262356 /// <summary>
263357 /// Reads the GraphQL response as a <see cref="OperationResult"/>.
@@ -270,7 +364,6 @@ public ValueTask<SourceResultDocument> ReadAsResultAsync(CancellationToken cance
270364 /// to read the <see cref="OperationResult"/> from the underlying <see cref="HttpResponseMessage"/>.
271365 /// </returns>
272366 public ValueTask < OperationResult > ReadAsResultAsync ( CancellationToken cancellationToken = default )
273- #endif
274367 {
275368 var contentType = _message . Content . Headers . ContentType ;
276369
@@ -293,6 +386,7 @@ public ValueTask<OperationResult> ReadAsResultAsync(CancellationToken cancellati
293386
294387 throw new InvalidOperationException ( "Received a successful response with an unexpected content type." ) ;
295388 }
389+ #endif
296390
297391#if FUSION
298392 private async ValueTask < SourceResultDocument > ReadAsResultInternalAsync ( string ? charSet , CancellationToken ct )
@@ -462,6 +556,43 @@ private async ValueTask<OperationResult> ReadAsResultInternalAsync(string? charS
462556 /// <see cref="HttpResponseMessage"/>.
463557 /// </returns>
464558 public IAsyncEnumerable < SourceResultDocument > ReadAsResultStreamAsync ( )
559+ {
560+ if ( ! TryGetRawMediaTypeAndCharSet ( out var mediaType , out var charSet ) )
561+ {
562+ _message . EnsureSuccessStatusCode ( ) ;
563+ throw new InvalidOperationException ( "Received a successful response with an unexpected content type." ) ;
564+ }
565+
566+ if ( mediaType . Equals ( ContentType . EventStream , StringComparison . OrdinalIgnoreCase ) )
567+ {
568+ return new SseReader ( _message ) ;
569+ }
570+
571+ if ( mediaType . Equals ( ContentType . GraphQLJsonLine , StringComparison . OrdinalIgnoreCase )
572+ || mediaType . Equals ( ContentType . JsonLine , StringComparison . OrdinalIgnoreCase ) )
573+ {
574+ return new JsonLinesReader ( _message ) ;
575+ }
576+
577+ // The server supports the newer graphql-response+json media type, and users are free
578+ // to use status codes.
579+ if ( mediaType . Equals ( ContentType . GraphQL , StringComparison . OrdinalIgnoreCase ) )
580+ {
581+ return new GraphQLHttpSingleResultEnumerable (
582+ ct => ReadAsResultInternalAsync ( charSet , ct ) ) ;
583+ }
584+
585+ _message . EnsureSuccessStatusCode ( ) ;
586+
587+ // The server supports the older application/json media type, and the status code
588+ // is expected to be a 2xx for a valid GraphQL response.
589+ if ( mediaType . Equals ( ContentType . Json , StringComparison . OrdinalIgnoreCase ) )
590+ {
591+ return new JsonResultEnumerable ( _message , charSet ) ;
592+ }
593+
594+ throw new InvalidOperationException ( "Received a successful response with an unexpected content type." ) ;
595+ }
465596#else
466597 /// <summary>
467598 /// Reads the GraphQL response as a <see cref="IAsyncEnumerable{T}"/> of <see cref="OperationResult"/>.
@@ -472,7 +603,6 @@ public IAsyncEnumerable<SourceResultDocument> ReadAsResultStreamAsync()
472603 /// <see cref="HttpResponseMessage"/>.
473604 /// </returns>
474605 public IAsyncEnumerable < OperationResult > ReadAsResultStreamAsync ( )
475- #endif
476606 {
477607 var contentType = _message . Content . Headers . ContentType ;
478608
@@ -508,6 +638,7 @@ public IAsyncEnumerable<OperationResult> ReadAsResultStreamAsync()
508638
509639 throw new InvalidOperationException ( "Received a successful response with an unexpected content type." ) ;
510640 }
641+ #endif
511642
512643 /// <summary>
513644 /// Disposes the underlying <see cref="HttpResponseMessage"/>.
0 commit comments