Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ namespace HotChocolate.Transport.Http;
/// </summary>
public sealed class DefaultGraphQLHttpClient : GraphQLHttpClient
{
#if FUSION
private const string JsonUtf8ContentType = $"{ContentType.Json}; charset=utf-8";
#endif

private readonly HttpClient _http;
private readonly bool _disposeInnerClient;

Expand Down Expand Up @@ -148,11 +152,22 @@ private static HttpRequestMessage CreateRequestMessage(
Method = method
};

#if FUSION
if (request.AcceptHeaderValue is not null)
{
message.Headers.TryAddWithoutValidation("Accept", request.AcceptHeaderValue);
}
else
{
#endif
message.Headers.Accept.Clear();
foreach (var contentType in request.Accept)
{
message.Headers.Accept.Add(contentType);
}
#if FUSION
}
#endif

if (method == GraphQLHttpMethod.Post)
{
Expand Down Expand Up @@ -192,7 +207,12 @@ private static ByteArrayContent CreatePostContent(

var internalBuffer = PooledArrayWriterMarshal.GetUnderlyingBuffer(arrayWriter);
var content = new ByteArrayContent(internalBuffer, 0, arrayWriter.Length);
#if FUSION
content.Headers.ContentType = null;
content.Headers.TryAddWithoutValidation("Content-Type", JsonUtf8ContentType);
#else
content.Headers.ContentType = new MediaTypeHeaderValue(ContentType.Json, "utf-8");
#endif
return content;
}

Expand All @@ -215,11 +235,21 @@ private static HttpContent CreateMultipartContent(
var form = new MultipartFormDataContent();

var operation = new ByteArrayContent(buffer, start, arrayWriter.Length - start);
#if FUSION
operation.Headers.ContentType = null;
operation.Headers.TryAddWithoutValidation("Content-Type", JsonUtf8ContentType);
#else
operation.Headers.ContentType = new MediaTypeHeaderValue(ContentType.Json, "utf-8");
#endif
form.Add(operation, "operations");

var fileMap = new ByteArrayContent(buffer, 0, start);
#if FUSION
fileMap.Headers.ContentType = null;
fileMap.Headers.TryAddWithoutValidation("Content-Type", JsonUtf8ContentType);
#else
fileMap.Headers.ContentType = new MediaTypeHeaderValue(ContentType.Json, "utf-8");
#endif
form.Add(fileMap, "map");

foreach (var fileInfo in fileInfos)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,14 @@ public GraphQLHttpRequest(OperationBatchRequest body, Uri? requestUri = null)
/// </summary>
public ImmutableArray<MediaTypeWithQualityHeaderValue> Accept { get; set; } = DefaultAcceptContentTypes;

#if FUSION
/// <summary>
/// Gets or sets a pre-formatted Accept header value to avoid per-request allocations.
/// When set, this is used instead of the typed <see cref="Accept"/> array.
/// </summary>
public string? AcceptHeaderValue { get; set; }
#endif

/// <summary>
/// Gets or sets a hook that can alter the <see cref="HttpRequestMessage"/> before it is sent.
/// </summary>
Expand Down
142 changes: 142 additions & 0 deletions src/HotChocolate/AspNetCore/src/Transport.Http/GraphQLHttpResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ namespace HotChocolate.Transport.Http;
public sealed class GraphQLHttpResponse : IDisposable
{
#if FUSION
private const string ContentTypeHeaderName = "Content-Type";
private const string CharsetPrefix = "charset=";
private const string Utf8 = "utf-8";
private const string JsonUtf8ContentType = $"{ContentType.Json}; charset={Utf8}";
private const string GraphQLUtf8ContentType = $"{ContentType.GraphQL}; charset={Utf8}";
private const string EventStreamUtf8ContentType = $"{ContentType.EventStream}; charset={Utf8}";
private const string GraphQLJsonLineUtf8ContentType = $"{ContentType.GraphQLJsonLine}; charset={Utf8}";
private const string JsonLineUtf8ContentType = $"{ContentType.JsonLine}; charset={Utf8}";

private static readonly StreamPipeReaderOptions s_options = new(
pool: MemoryPool<byte>.Shared,
bufferSize: 4096,
Expand Down Expand Up @@ -93,6 +102,139 @@ public GraphQLHttpResponse(HttpResponseMessage message)
/// </returns>
public HttpContentHeaders ContentHeaders => _message.Content.Headers;

#if FUSION
/// <summary>
/// Gets the raw Content-Type header value without parsing into <see cref="MediaTypeHeaderValue"/>.
/// </summary>
public string? RawContentType
{
get
{
if (_message.Content.Headers.NonValidated.TryGetValues(ContentTypeHeaderName, out var values))
{
var enumerator = values.GetEnumerator();
if (!enumerator.MoveNext())
{
return null;
}

var mediaType = enumerator.Current;

// Some handlers may emit media type and charset as separate values.
// Normalize known UTF-8 combinations back to shared constants.
if (enumerator.MoveNext()
&& TryNormalizeKnownUtf8ContentType(mediaType.AsSpan(), enumerator.Current.AsSpan(), out var normalized))
{
return normalized;
}

return mediaType;
}

return null;
}
}

private static bool TryNormalizeKnownUtf8ContentType(
ReadOnlySpan<char> mediaType,
ReadOnlySpan<char> charset,
out string contentType)
{
if (!IsUtf8(charset))
{
contentType = null!;
return false;
}

mediaType = NormalizeMediaType(mediaType);

if (mediaType.Equals(ContentType.GraphQL, StringComparison.OrdinalIgnoreCase))
{
contentType = GraphQLUtf8ContentType;
return true;
}

if (mediaType.Equals(ContentType.JsonLine, StringComparison.OrdinalIgnoreCase))
{
contentType = JsonLineUtf8ContentType;
return true;
}

if (mediaType.Equals(ContentType.Json, StringComparison.OrdinalIgnoreCase))
{
contentType = JsonUtf8ContentType;
return true;
}

if (mediaType.Equals(ContentType.EventStream, StringComparison.OrdinalIgnoreCase))
{
contentType = EventStreamUtf8ContentType;
return true;
}

if (mediaType.Equals(ContentType.GraphQLJsonLine, StringComparison.OrdinalIgnoreCase))
{
contentType = GraphQLJsonLineUtf8ContentType;
return true;
}

contentType = null!;
return false;
}

private static bool IsUtf8(ReadOnlySpan<char> value)
{
value = TrimWhiteSpace(value);

if (value.Length > 0 && value[0] == ';')
{
value = TrimWhiteSpace(value[1..]);
}

if (value.StartsWith(CharsetPrefix, StringComparison.OrdinalIgnoreCase))
{
value = TrimWhiteSpace(value[CharsetPrefix.Length..]);
}

if (value.Length > 1 && value[0] == '"' && value[^1] == '"')
{
value = TrimWhiteSpace(value[1..^1]);
}

return value.Equals(Utf8, StringComparison.OrdinalIgnoreCase);
}

private static ReadOnlySpan<char> NormalizeMediaType(ReadOnlySpan<char> mediaType)
{
mediaType = TrimWhiteSpace(mediaType);

if (mediaType.Length > 0 && mediaType[^1] == ';')
{
mediaType = TrimWhiteSpace(mediaType[..^1]);
}

return mediaType;
}

private static ReadOnlySpan<char> TrimWhiteSpace(ReadOnlySpan<char> value)
{
var start = 0;
var end = value.Length - 1;

while (start <= end && char.IsWhiteSpace(value[start]))
{
start++;
}

while (end >= start && char.IsWhiteSpace(value[end]))
{
end--;
}

return value[start..(end + 1)];
}
#endif

/// <summary>
/// Gets the collection of trailing headers included in an HTTP response.
/// </summary>
Expand Down
Empty file modified src/HotChocolate/Fusion/benchmarks/k6/start-gateway.sh
100644 → 100755
Empty file.
Empty file modified src/HotChocolate/Fusion/benchmarks/k6/start-source-schemas.sh
100644 → 100755
Empty file.
Empty file modified src/HotChocolate/Fusion/benchmarks/k6/stop-services.sh
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public async ValueTask<ImmutableArray<SourceSchemaClientResponse>> ExecuteBatchA
var httpResponse = await _client.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);

var uri = httpRequest.Uri ?? new Uri("http://unknown");
var contentType = httpResponse.ContentHeaders.ContentType?.ToString() ?? "unknown";
var contentType = httpResponse.RawContentType ?? "unknown";
var isSuccessful = httpResponse.IsSuccessStatusCode;

var nodeResponses = new NodeResponse[requests.Length];
Expand Down Expand Up @@ -145,9 +145,9 @@ public async ValueTask<ImmutableArray<SourceSchemaClientResponse>> ExecuteBatchA
private GraphQLHttpRequest CreateHttpRequest(
SourceSchemaClientRequest originalRequest)
{
var defaultAccept = originalRequest.OperationType is OperationType.Subscription
? _configuration.SubscriptionAcceptHeaderValues
: _configuration.DefaultAcceptHeaderValues;
var defaultAcceptHeader = originalRequest.OperationType is OperationType.Subscription
? _configuration.SubscriptionAcceptHeaderValue
: _configuration.DefaultAcceptHeaderValue;
var operationSourceText = originalRequest.OperationSourceText;

switch (originalRequest.Variables.Length)
Expand All @@ -156,15 +156,15 @@ private GraphQLHttpRequest CreateHttpRequest(
return new GraphQLHttpRequest(CreateSingleRequest(operationSourceText))
{
Uri = _configuration.BaseAddress,
Accept = defaultAccept
AcceptHeaderValue = defaultAcceptHeader
};

case 1:
var variableValues = originalRequest.Variables[0].Values;
return new GraphQLHttpRequest(CreateSingleRequest(operationSourceText, variableValues))
{
Uri = _configuration.BaseAddress,
Accept = defaultAccept,
AcceptHeaderValue = defaultAcceptHeader,
EnableFileUploads = originalRequest.RequiresFileUpload
};

Expand All @@ -174,15 +174,15 @@ private GraphQLHttpRequest CreateHttpRequest(
return new GraphQLHttpRequest(CreateOperationBatchRequest(operationSourceText, originalRequest))
{
Uri = _configuration.BaseAddress,
Accept = _configuration.BatchingAcceptHeaderValues,
AcceptHeaderValue = _configuration.BatchingAcceptHeaderValue,
EnableFileUploads = originalRequest.RequiresFileUpload
};
}

return new GraphQLHttpRequest(CreateVariableBatchRequest(operationSourceText, originalRequest))
{
Uri = _configuration.BaseAddress,
Accept = _configuration.BatchingAcceptHeaderValues,
AcceptHeaderValue = _configuration.BatchingAcceptHeaderValue,
EnableFileUploads = originalRequest.RequiresFileUpload
};
}
Expand Down Expand Up @@ -214,7 +214,7 @@ private GraphQLHttpRequest CreateHttpBatchRequest(
return new GraphQLHttpRequest(new OperationBatchRequest(batchRequests))
{
Uri = _configuration.BaseAddress,
Accept = _configuration.BatchingAcceptHeaderValues,
AcceptHeaderValue = _configuration.BatchingAcceptHeaderValue,
EnableFileUploads = enableFileUploads
};
}
Expand Down Expand Up @@ -547,7 +547,7 @@ private sealed class Response(
{
public override Uri Uri => request.Uri ?? new Uri("http://unknown");

public override string ContentType => response.ContentHeaders.ContentType?.ToString() ?? "unknown";
public override string ContentType => response.RawContentType ?? "unknown";

public override bool IsSuccessful => response.IsSuccessStatusCode;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ public SourceSchemaHttpClientConfiguration(
BatchingMode = batchingMode;

DefaultAcceptHeaderValues = defaultAcceptHeaderValues ?? AcceptContentTypes.Default;
DefaultAcceptHeaderValue = defaultAcceptHeaderValues.HasValue
? AcceptContentTypes.FormatAcceptHeader(defaultAcceptHeaderValues.Value)
: AcceptContentTypes.DefaultHeader;

if (batchingMode.HasFlag(SourceSchemaHttpClientBatchingMode.VariableBatching))
{
Expand All @@ -144,15 +147,22 @@ public SourceSchemaHttpClientConfiguration(
if (batchingAcceptHeaderValues.HasValue)
{
BatchingAcceptHeaderValues = batchingAcceptHeaderValues.Value;
BatchingAcceptHeaderValue = AcceptContentTypes.FormatAcceptHeader(batchingAcceptHeaderValues.Value);
}
else
{
BatchingAcceptHeaderValues = batchingMode == SourceSchemaHttpClientBatchingMode.ApolloRequestBatching
? AcceptContentTypes.ApolloRequestBatching
: AcceptContentTypes.VariableBatching;
BatchingAcceptHeaderValue = batchingMode == SourceSchemaHttpClientBatchingMode.ApolloRequestBatching
? AcceptContentTypes.ApolloRequestBatchingHeader
: AcceptContentTypes.VariableBatchingHeader;
}

SubscriptionAcceptHeaderValues = subscriptionAcceptHeaderValues ?? AcceptContentTypes.Subscription;
SubscriptionAcceptHeaderValue = subscriptionAcceptHeaderValues.HasValue
? AcceptContentTypes.FormatAcceptHeader(subscriptionAcceptHeaderValues.Value)
: AcceptContentTypes.SubscriptionHeader;

OnBeforeSend = onBeforeSend;
OnAfterReceive = onAfterReceive;
Expand Down Expand Up @@ -189,16 +199,31 @@ public SourceSchemaHttpClientConfiguration(
/// </summary>
public ImmutableArray<MediaTypeWithQualityHeaderValue> DefaultAcceptHeaderValues { get; }

/// <summary>
/// Gets a pre-formatted Accept header string for single, non-Subscription GraphQL requests.
/// </summary>
public string DefaultAcceptHeaderValue { get; }

/// <summary>
/// Gets the <c>Accept</c> header values sent in case of a batching request.
/// </summary>
public ImmutableArray<MediaTypeWithQualityHeaderValue> BatchingAcceptHeaderValues { get; }

/// <summary>
/// Gets a pre-formatted Accept header string for batching requests.
/// </summary>
public string BatchingAcceptHeaderValue { get; }

/// <summary>
/// Gets the <c>Accept</c> header values sent in case of a subscription.
/// </summary>
public ImmutableArray<MediaTypeWithQualityHeaderValue> SubscriptionAcceptHeaderValues { get; }

/// <summary>
/// Gets a pre-formatted Accept header string for subscriptions.
/// </summary>
public string SubscriptionAcceptHeaderValue { get; }

/// <summary>
/// Called before the request is sent.
/// </summary>
Expand Down Expand Up @@ -242,5 +267,16 @@ private static class AcceptContentTypes
new("application/jsonl") { CharSet = "utf-8" },
new("text/event-stream") { CharSet = "utf-8" }
];

public static string DefaultHeader { get; } = FormatAcceptHeader(Default);

public static string VariableBatchingHeader { get; } = FormatAcceptHeader(VariableBatching);

public static string ApolloRequestBatchingHeader { get; } = FormatAcceptHeader(ApolloRequestBatching);

public static string SubscriptionHeader { get; } = FormatAcceptHeader(Subscription);

public static string FormatAcceptHeader(ImmutableArray<MediaTypeWithQualityHeaderValue> values)
=> string.Join(", ", values);
}
}
Loading
Loading