diff --git a/src/Grpc.Net.Client/GrpcChannel.cs b/src/Grpc.Net.Client/GrpcChannel.cs index 15ba219e2..8ada9df9a 100644 --- a/src/Grpc.Net.Client/GrpcChannel.cs +++ b/src/Grpc.Net.Client/GrpcChannel.cs @@ -92,7 +92,7 @@ public sealed class GrpcChannel : ChannelBase, IDisposable // Options that are set in unit tests internal ISystemClock Clock = SystemClock.Instance; - internal IOperatingSystem OperatingSystem = Internal.OperatingSystem.Instance; + internal IOperatingSystem OperatingSystem; internal IRandomGenerator RandomGenerator; internal bool DisableClientDeadline; internal long MaxTimerDueTime = uint.MaxValue - 1; // Max System.Threading.Timer due time @@ -112,6 +112,7 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr Address = address; LoggerFactory = channelOptions.LoggerFactory ?? channelOptions.ResolveService(NullLoggerFactory.Instance); + OperatingSystem = channelOptions.ResolveService(Internal.OperatingSystem.Instance); RandomGenerator = channelOptions.ResolveService(new RandomGenerator()); (HttpHandlerType, ConnectTimeout) = CalculateHandlerContext(channelOptions); @@ -382,6 +383,31 @@ private HttpMessageInvoker CreateInternalHttpInvoker(HttpMessageHandler? handler { handler = HttpHandlerFactory.CreatePrimaryHandler(); } + else + { + // Validate the user specified handler is compatible with this platform. + // + // Android's native handler doesn't fully support HTTP/2 and using it could cause hard to understand errors + // in advanced gRPC scenarios. We want Android to use SocketsHttpHandler. Throw an error if: + // 1. Client is running on Android. + // 2. Channel is created with HttpClientHandler. + // 3. UseNativeHttpHandler switch is true. + if (OperatingSystem.IsAndroid) + { + // GetHttpHandlerType recurses through DelegatingHandlers that may wrap the HttpClientHandler. + var httpClientHandler = HttpRequestHelpers.GetHttpHandlerType(handler); + + if (httpClientHandler != null && RuntimeHelpers.QueryRuntimeSettingSwitch("System.Net.Http.UseNativeHttpHandler", defaultValue: false)) + { + throw new InvalidOperationException("The channel configuration isn't valid on Android devices. " + + "The channel is configured to use HttpClientHandler and Android's native HTTP/2 library. " + + "gRPC isn't fully supported by Android's native HTTP/2 library and it can cause runtime errors. " + + "To fix this problem, either configure the channel to use SocketsHttpHandler, or add " + + "false to the app's project file. " + + "For more information, see https://aka.ms/aspnet/grpc/android."); + } + } + } #if NET5_0 handler = HttpHandlerFactory.EnsureTelemetryHandler(handler); diff --git a/src/Grpc.Net.Client/Internal/OperatingSystem.cs b/src/Grpc.Net.Client/Internal/OperatingSystem.cs index 24a25e363..836771090 100644 --- a/src/Grpc.Net.Client/Internal/OperatingSystem.cs +++ b/src/Grpc.Net.Client/Internal/OperatingSystem.cs @@ -23,16 +23,23 @@ namespace Grpc.Net.Client.Internal; internal interface IOperatingSystem { bool IsBrowser { get; } + bool IsAndroid { get; } } -internal class OperatingSystem : IOperatingSystem +internal sealed class OperatingSystem : IOperatingSystem { public static readonly OperatingSystem Instance = new OperatingSystem(); public bool IsBrowser { get; } + public bool IsAndroid { get; } private OperatingSystem() { IsBrowser = RuntimeInformation.IsOSPlatform(OSPlatform.Create("browser")); +#if NET5_0_OR_GREATER + IsAndroid = System.OperatingSystem.IsAndroid(); +#else + IsAndroid = false; +#endif } } diff --git a/src/Grpc.Net.Client/Internal/RuntimeHelpers.cs b/src/Grpc.Net.Client/Internal/RuntimeHelpers.cs new file mode 100644 index 000000000..73648fcb5 --- /dev/null +++ b/src/Grpc.Net.Client/Internal/RuntimeHelpers.cs @@ -0,0 +1,33 @@ +#region Copyright notice and license + +// Copyright 2019 The gRPC Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#endregion + + +namespace Grpc.Net.Client.Internal; + +internal static class RuntimeHelpers +{ + public static bool QueryRuntimeSettingSwitch(string switchName, bool defaultValue) + { + if (AppContext.TryGetSwitch(switchName, out var value)) + { + return value; + } + + return defaultValue; + } +} diff --git a/test/Grpc.Net.Client.Tests/GetStatusTests.cs b/test/Grpc.Net.Client.Tests/GetStatusTests.cs index cd613c364..87e0bcb3f 100644 --- a/test/Grpc.Net.Client.Tests/GetStatusTests.cs +++ b/test/Grpc.Net.Client.Tests/GetStatusTests.cs @@ -213,6 +213,7 @@ public async Task AsyncUnaryCall_MissingStatusBrowser_ThrowError() private class TestOperatingSystem : IOperatingSystem { public bool IsBrowser { get; set; } + public bool IsAndroid { get; set; } } [Test] diff --git a/test/Grpc.Net.Client.Tests/GrpcChannelTests.cs b/test/Grpc.Net.Client.Tests/GrpcChannelTests.cs index 96dce2418..59d89920e 100644 --- a/test/Grpc.Net.Client.Tests/GrpcChannelTests.cs +++ b/test/Grpc.Net.Client.Tests/GrpcChannelTests.cs @@ -26,6 +26,7 @@ using Microsoft.Extensions.Logging.Testing; using NUnit.Framework; using Grpc.Net.Client.Internal; +using System.Net; #if SUPPORT_LOAD_BALANCING using Grpc.Net.Client.Balancer; using Grpc.Net.Client.Balancer.Internal; @@ -401,6 +402,87 @@ public async Task Dispose_CalledWhileActiveCalls_ActiveCallsDisposed() Assert.AreEqual(0, channel.ActiveCalls.Count); } + [TestCase(null)] + [TestCase(false)] + public void HttpHandler_HttpClientHandlerOverNativeOnAndroid_ThrowError(bool useDelegatingHandlers) + { + // Arrange + AppContext.SetSwitch("System.Net.Http.UseNativeHttpHandler", true); + + try + { + var services = new ServiceCollection(); + services.AddSingleton(new TestOperatingSystem { IsAndroid = true }); + + HttpMessageHandler handler = new HttpClientHandler(); + if (useDelegatingHandlers) + { + handler = new TestDelegatingHandler(handler); + } + + var ex = Assert.Throws(() => + { + GrpcChannel.ForAddress("https://localhost", new GrpcChannelOptions + { + HttpHandler = handler, + ServiceProvider = services.BuildServiceProvider() + }); + }); + + Assert.AreEqual(ex!.Message, "The channel configuration isn't valid on Android devices. " + + "The channel is configured to use HttpClientHandler and Android's native HTTP/2 library. " + + "gRPC isn't fully supported by Android's native HTTP/2 library and it can cause runtime errors. " + + "To fix this problem, either configure the channel to use SocketsHttpHandler, or add " + + "false to the app's project file. " + + "For more information, see https://aka.ms/aspnet/grpc/android."); + } + finally + { + // Reset switch for other tests. + AppContext.SetSwitch("System.Net.Http.UseNativeHttpHandler", false); + } + } + + private class TestDelegatingHandler : DelegatingHandler + { + public TestDelegatingHandler(HttpMessageHandler innerHandler) : base(innerHandler) + { + } + } + + [Test] + [TestCase(null)] + [TestCase(false)] + public void HttpHandler_HttpClientHandlerOverSocketsOnAndroid_Success(bool? isNativeHttpHandler) + { + // Arrange + if (isNativeHttpHandler != null) + { + AppContext.SetSwitch("System.Net.Http.UseNativeHttpHandler", isNativeHttpHandler.Value); + } + + var services = new ServiceCollection(); + services.AddSingleton(new TestOperatingSystem { IsAndroid = true }); + + var handler = new HttpClientHandler(); + + // Act + var channel = GrpcChannel.ForAddress("https://localhost", new GrpcChannelOptions + { + HttpHandler = handler, + ServiceProvider = services.BuildServiceProvider() + }); + + // Assert + Assert.IsTrue(channel.OperatingSystem.IsAndroid); + } + + private class TestOperatingSystem : IOperatingSystem + { + public bool IsBrowser { get; set; } + public bool IsAndroid { get; set; } + } + #if SUPPORT_LOAD_BALANCING [Test] public void Resolver_SocketHttpHandlerWithConnectCallback_Error()