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

Detect native HttpHandler on Android #1927

Merged
merged 1 commit into from
Nov 1, 2022
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
28 changes: 27 additions & 1 deletion src/Grpc.Net.Client/GrpcChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -112,6 +112,7 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr

Address = address;
LoggerFactory = channelOptions.LoggerFactory ?? channelOptions.ResolveService<ILoggerFactory>(NullLoggerFactory.Instance);
OperatingSystem = channelOptions.ResolveService<IOperatingSystem>(Internal.OperatingSystem.Instance);
RandomGenerator = channelOptions.ResolveService<IRandomGenerator>(new RandomGenerator());
(HttpHandlerType, ConnectTimeout) = CalculateHandlerContext(channelOptions);

Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this mean the native handler works for some cases? And what if the native handler improves in the future to have better support? Should there be a switch to ignore this check and allow using the native handler?

Copy link
Member Author

Choose a reason for hiding this comment

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

My thought is if that happens, the grpc-dotnet client can be updated to detect the Android OS version that fully supports gRPC, and not throw this error.

// 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<HttpClientHandler>(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 " +
"<UseNativeHttpHandler>false</UseNativeHttpHandler> to the app's project file. " +
"For more information, see https://aka.ms/aspnet/grpc/android.");
}
}
}

#if NET5_0
handler = HttpHandlerFactory.EnsureTelemetryHandler(handler);
Expand Down
9 changes: 8 additions & 1 deletion src/Grpc.Net.Client/Internal/OperatingSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
33 changes: 33 additions & 0 deletions src/Grpc.Net.Client/Internal/RuntimeHelpers.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions test/Grpc.Net.Client.Tests/GetStatusTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
82 changes: 82 additions & 0 deletions test/Grpc.Net.Client.Tests/GrpcChannelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<IOperatingSystem>(new TestOperatingSystem { IsAndroid = true });

HttpMessageHandler handler = new HttpClientHandler();
if (useDelegatingHandlers)
{
handler = new TestDelegatingHandler(handler);
}

var ex = Assert.Throws<InvalidOperationException>(() =>
{
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 " +
"<UseNativeHttpHandler>false</UseNativeHttpHandler> 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<IOperatingSystem>(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()
Expand Down