Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HTTP version configuration to GrpcChannelOptions #2514

Merged
merged 5 commits into from
Aug 21, 2024
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
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.")]
JamesNK marked this conversation as resolved.
Show resolved Hide resolved
#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; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume something blows up downstream if someone sets 2.5 or 7.0, but what happens if they select 1.1?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the tests, it seems like this is handled somewhere (or might even be allowable?).

Copy link
Member Author

@JamesNK JamesNK Aug 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1.1 will successfully create a connection if the server supports 1.1 and you're using gRPC-Web.

If you select a non-standard version then I'm guessing SocketsHttpHandler will error and tell you you're using a bad version.


#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