Skip to content

Commit

Permalink
Add HTTP version configuration to GrpcChannelOptions (#2514)
Browse files Browse the repository at this point in the history
Co-authored-by: Andrew Casey <[email protected]>
  • Loading branch information
JamesNK and amcasey authored Aug 21, 2024
1 parent 26227c3 commit e9cc7e1
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 121 deletions.
25 changes: 5 additions & 20 deletions perf/benchmarkapps/GrpcClient/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand Down Expand Up @@ -497,13 +497,14 @@ private static ChannelBase CreateChannel(string target)

HttpMessageHandler httpMessageHandler = httpClientHandler;

Version? versionOverride = null;
if (_options.Protocol == "h3")
{
// Stop gRPC channel from creating TCP socket.
httpClientHandler.ConnectCallback = (context, cancellationToken) => throw new InvalidOperationException("Should never be called for H3.");

// Force H3 on all requests.
httpMessageHandler = new Http3DelegatingHandler(httpMessageHandler);
versionOverride = new Version(3, 0);
}

return GrpcChannel.ForAddress(address, new GrpcChannelOptions
Expand All @@ -513,7 +514,8 @@ private static ChannelBase CreateChannel(string target)
#else
HttpClient = new HttpClient(httpMessageHandler),
#endif
LoggerFactory = _loggerFactory
LoggerFactory = _loggerFactory,
HttpVersion = versionOverride
});
}
}
Expand Down Expand Up @@ -759,21 +761,4 @@ private static bool IsCallCountExceeded()
{
return _options.CallCount != null && _callsStarted > _options.CallCount;
}

private class Http3DelegatingHandler : DelegatingHandler
{
private static readonly Version Http3Version = new Version(3, 0);

public Http3DelegatingHandler(HttpMessageHandler innerHandler)
{
InnerHandler = innerHandler;
}

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Version = Http3Version;
request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
return base.SendAsync(request, cancellationToken);
}
}
}
22 changes: 16 additions & 6 deletions src/Grpc.Net.Client.Web/GrpcWebHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand Down Expand Up @@ -44,6 +44,11 @@ public sealed class GrpcWebHandler : DelegatingHandler
/// be overridden.
/// </para>
/// </summary>
#if NET5_0_OR_GREATER
[Obsolete("HttpVersion is obsolete and will be removed in a future release. Use GrpcChannelOptions.HttpVersion and GrpcChannelOptions.HttpVersionPolicy instead.")]
#else
[Obsolete("HttpVersion is obsolete and will be removed in a future release. Use GrpcChannelOptions.HttpVersion instead.")]
#endif
public Version? HttpVersion { get; set; }

/// <summary>
Expand Down Expand Up @@ -136,22 +141,27 @@ private async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request
// https://github.com/mono/mono/issues/18718
request.SetOption(WebAssemblyEnableStreamingResponseKey, true);

#pragma warning disable CS0618 // Type or member is obsolete
if (HttpVersion != null)
{
// This doesn't guarantee that the specified version is used. Some handlers will ignore it.
// For example, version in the browser always negotiated by the browser and HttpClient
// uses what the browser has negotiated.
request.Version = HttpVersion;
#if NET5_0_OR_GREATER
request.VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
#endif
}
#pragma warning restore CS0618 // Type or member is obsolete
#if NET5_0_OR_GREATER
else if (request.RequestUri?.Scheme == Uri.UriSchemeHttps
&& request.VersionPolicy == HttpVersionPolicy.RequestVersionOrHigher
&& request.VersionPolicy == HttpVersionPolicy.RequestVersionExact
&& request.Version == System.Net.HttpVersion.Version20)
{
// If no explicit HttpVersion is set and the request is using TLS then default to HTTP/1.1.
// HTTP/1.1 together with HttpVersionPolicy.RequestVersionOrHigher it will be compatible
// with all endpoints.
request.Version = System.Net.HttpVersion.Version11;
// If no explicit HttpVersion is set and the request is using TLS then change the version policy
// to allow for HTTP/1.1. HttpVersionPolicy.RequestVersionOrLower it will be compatible
// with HTTP/1.1 and HTTP/2.
request.VersionPolicy = HttpVersionPolicy.RequestVersionOrLower;
}
#endif
#if NETSTANDARD2_0
Expand Down
8 changes: 8 additions & 0 deletions src/Grpc.Net.Client/GrpcChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ public sealed class GrpcChannel : ChannelBase, IDisposable
internal Dictionary<string, ICompressionProvider> CompressionProviders { get; }
internal string MessageAcceptEncoding { get; }
internal bool Disposed { get; private set; }
internal Version HttpVersion { get; }
#if NET5_0_OR_GREATER
internal HttpVersionPolicy HttpVersionPolicy { get; }
#endif

#if SUPPORT_LOAD_BALANCING
// Load balancing
Expand Down Expand Up @@ -175,6 +179,10 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr
RetryThrottling = serviceConfig.RetryThrottling != null ? CreateChannelRetryThrottling(serviceConfig.RetryThrottling) : null;
_serviceConfigMethods = CreateServiceConfigMethods(serviceConfig);
}
HttpVersion = channelOptions.HttpVersion ?? GrpcProtocolConstants.Http2Version;
#if NET5_0_OR_GREATER
HttpVersionPolicy = channelOptions.HttpVersionPolicy ?? HttpVersionPolicy.RequestVersionExact;
#endif

// Non-HTTP addresses (e.g. dns:///custom-hostname) usually specify a path instead of an authority.
// Only log about a path being present if HTTP or HTTPS.
Expand Down
31 changes: 30 additions & 1 deletion src/Grpc.Net.Client/GrpcChannelOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand Down Expand Up @@ -287,6 +287,35 @@ public TimeSpan InitialReconnectBackoff
/// </summary>
public IServiceProvider? ServiceProvider { get; set; }

/// <summary>
/// Gets or sets the HTTP version to use when making gRPC calls.
/// <para>
/// When a <see cref="Version"/> is specified the value will be set on <see cref="HttpRequestMessage.Version"/>
/// as gRPC calls are made. Changing this property allows the HTTP version of gRPC calls to
/// be overridden.
/// </para>
/// <para>
/// A <c>null</c> value doesn't override the HTTP version of gRPC calls. Defaults to <c>2.0</c>.
/// </para>
/// </summary>
public Version? HttpVersion { get; set; }

#if NET5_0_OR_GREATER
/// <summary>
/// Gets or sets the HTTP policy to use when making gRPC calls.
/// <para>
/// When a <see cref="HttpVersionPolicy"/> is specified the value will be set on <see cref="HttpRequestMessage.VersionPolicy"/>
/// as gRPC calls are made. The policy determines how <see cref="Version"/> is interpreted when
/// the final HTTP version is negotiated with the server. Changing this property allows the HTTP
/// version of gRPC calls to be overridden.
/// </para>
/// <para>
/// A <c>null</c> value doesn't override the HTTP policy of gRPC calls. Defaults to <see cref="HttpVersionPolicy.RequestVersionExact"/>.
/// </para>
/// </summary>
public HttpVersionPolicy? HttpVersionPolicy { get; set; }
#endif

internal T ResolveService<T>(T defaultValue)
{
return (T?)ServiceProvider?.GetService(typeof(T)) ?? defaultValue;
Expand Down
4 changes: 2 additions & 2 deletions src/Grpc.Net.Client/Internal/GrpcCall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -957,9 +957,9 @@ private void SetWriter(HttpRequestMessage message, HttpContentClientStreamWriter
private HttpRequestMessage CreateHttpRequestMessage(TimeSpan? timeout)
{
var message = new HttpRequestMessage(HttpMethod.Post, _grpcMethodInfo.CallUri);
message.Version = GrpcProtocolConstants.Http2Version;
message.Version = Channel.HttpVersion;
#if NET5_0_OR_GREATER
message.VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
message.VersionPolicy = Channel.HttpVersionPolicy;
#endif

// Set raw headers on request using name/values. Typed headers allocate additional objects.
Expand Down
13 changes: 10 additions & 3 deletions test/FunctionalTests/Balancer/ConnectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ async Task<HelloReply> UnaryMethod(HelloRequest request, ServerCallContext conte
var client = TestClientFactory.Create(channel, endpoint1.Method);

// Act
grpcWebHandler.HttpVersion = new Version(1, 1);
SetHandlerHttpVersion(grpcWebHandler, new Version(1, 1));
var http11CallTasks = new List<Task<HelloReply>>();
for (int i = 0; i < 10; i++)
{
Expand Down Expand Up @@ -352,7 +352,7 @@ await TestHelpers.AssertIsTrueRetryAsync(() =>
}

// Act
grpcWebHandler.HttpVersion = new Version(2, 0);
SetHandlerHttpVersion(grpcWebHandler, new Version(2, 0));
var http2CallTasks = new List<Task<HelloReply>>();
for (int i = 0; i < 10; i++)
{
Expand Down Expand Up @@ -389,7 +389,7 @@ await TestHelpers.AssertIsTrueRetryAsync(() =>
return activeStreams.Count == 10;
}, "Wait for HTTP/2 connection to end.");

grpcWebHandler.HttpVersion = new Version(1, 1);
SetHandlerHttpVersion(grpcWebHandler, new Version(1, 1));

await Task.Delay(1000);

Expand All @@ -412,6 +412,13 @@ await TestHelpers.AssertIsTrueRetryAsync(() =>
Assert.AreEqual(new DnsEndPoint("127.0.0.1", endpoint2.Address.Port), activeStreams[0].EndPoint);
}

private static void SetHandlerHttpVersion(GrpcWebHandler handler, Version version)
{
#pragma warning disable CS0618 // Type or member is obsolete
handler.HttpVersion = version;
#pragma warning restore CS0618 // Type or member is obsolete
}

#if NET7_0_OR_GREATER
[Test]
public async Task Active_UnaryCall_HostOverride_Success()
Expand Down
15 changes: 0 additions & 15 deletions test/FunctionalTests/Balancer/PickFirstBalancerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,6 @@ namespace Grpc.AspNetCore.FunctionalTests.Balancer;
[TestFixture]
public class PickFirstBalancerTests : FunctionalTestBase
{
private GrpcChannel CreateGrpcWebChannel(TestServerEndpointName endpointName, Version? version)
{
var grpcWebHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb);
grpcWebHandler.HttpVersion = version;

var httpClient = Fixture.CreateClient(endpointName, grpcWebHandler);
var channel = GrpcChannel.ForAddress(httpClient.BaseAddress!, new GrpcChannelOptions
{
HttpClient = httpClient,
LoggerFactory = LoggerFactory
});

return channel;
}

[Test]
public async Task UnaryCall_CallAfterConnectionTimeout_Success()
{
Expand Down
21 changes: 5 additions & 16 deletions test/FunctionalTests/Client/ClientFactoryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ Task<HelloReply> UnaryCall(HelloRequest request, ServerCallContext context)
{
return TestClientFactory.Create(invoker, method);
})
.AddHttpMessageHandler(() => new Http3Handler())
.ConfigureChannel(options =>
{
options.HttpVersion = HttpVersion.Version30;
options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new SocketsHttpHandler
Expand All @@ -125,20 +129,5 @@ Task<HelloReply> UnaryCall(HelloRequest request, ServerCallContext context)
// Assert
Assert.AreEqual("Hello world", response1.Message);
}

private class Http3Handler : DelegatingHandler
{
public Http3Handler() { }
public Http3Handler(HttpMessageHandler innerHandler) : base(innerHandler) { }

protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Version = HttpVersion.Version30;
request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;

return base.SendAsync(request, cancellationToken);
}
}
#endif
}
5 changes: 2 additions & 3 deletions test/FunctionalTests/Infrastructure/GrpcHttpHelper.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#region Copyright notice and license
#region Copyright notice and license

// Copyright 2019 The gRPC Authors
//
Expand All @@ -16,7 +16,6 @@

#endregion


namespace Grpc.AspNetCore.FunctionalTests.Infrastructure;

public static class GrpcHttpHelper
Expand All @@ -26,7 +25,7 @@ public static HttpRequestMessage Create(string url, HttpMethod? method = null)
var request = new HttpRequestMessage(method ?? HttpMethod.Post, url);
request.Version = new Version(2, 0);
#if NET5_0_OR_GREATER
request.VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
#endif

return request;
Expand Down
Loading

0 comments on commit e9cc7e1

Please sign in to comment.