Skip to content

Commit 7154cb4

Browse files
authored
Detect native HttpHandler on Android (#1927)
1 parent 4c2e66e commit 7154cb4

File tree

5 files changed

+151
-2
lines changed

5 files changed

+151
-2
lines changed

src/Grpc.Net.Client/GrpcChannel.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public sealed class GrpcChannel : ChannelBase, IDisposable
9292

9393
// Options that are set in unit tests
9494
internal ISystemClock Clock = SystemClock.Instance;
95-
internal IOperatingSystem OperatingSystem = Internal.OperatingSystem.Instance;
95+
internal IOperatingSystem OperatingSystem;
9696
internal IRandomGenerator RandomGenerator;
9797
internal bool DisableClientDeadline;
9898
internal long MaxTimerDueTime = uint.MaxValue - 1; // Max System.Threading.Timer due time
@@ -112,6 +112,7 @@ internal GrpcChannel(Uri address, GrpcChannelOptions channelOptions) : base(addr
112112

113113
Address = address;
114114
LoggerFactory = channelOptions.LoggerFactory ?? channelOptions.ResolveService<ILoggerFactory>(NullLoggerFactory.Instance);
115+
OperatingSystem = channelOptions.ResolveService<IOperatingSystem>(Internal.OperatingSystem.Instance);
115116
RandomGenerator = channelOptions.ResolveService<IRandomGenerator>(new RandomGenerator());
116117
(HttpHandlerType, ConnectTimeout) = CalculateHandlerContext(channelOptions);
117118

@@ -382,6 +383,31 @@ private HttpMessageInvoker CreateInternalHttpInvoker(HttpMessageHandler? handler
382383
{
383384
handler = HttpHandlerFactory.CreatePrimaryHandler();
384385
}
386+
else
387+
{
388+
// Validate the user specified handler is compatible with this platform.
389+
//
390+
// Android's native handler doesn't fully support HTTP/2 and using it could cause hard to understand errors
391+
// in advanced gRPC scenarios. We want Android to use SocketsHttpHandler. Throw an error if:
392+
// 1. Client is running on Android.
393+
// 2. Channel is created with HttpClientHandler.
394+
// 3. UseNativeHttpHandler switch is true.
395+
if (OperatingSystem.IsAndroid)
396+
{
397+
// GetHttpHandlerType recurses through DelegatingHandlers that may wrap the HttpClientHandler.
398+
var httpClientHandler = HttpRequestHelpers.GetHttpHandlerType<HttpClientHandler>(handler);
399+
400+
if (httpClientHandler != null && RuntimeHelpers.QueryRuntimeSettingSwitch("System.Net.Http.UseNativeHttpHandler", defaultValue: false))
401+
{
402+
throw new InvalidOperationException("The channel configuration isn't valid on Android devices. " +
403+
"The channel is configured to use HttpClientHandler and Android's native HTTP/2 library. " +
404+
"gRPC isn't fully supported by Android's native HTTP/2 library and it can cause runtime errors. " +
405+
"To fix this problem, either configure the channel to use SocketsHttpHandler, or add " +
406+
"<UseNativeHttpHandler>false</UseNativeHttpHandler> to the app's project file. " +
407+
"For more information, see https://aka.ms/aspnet/grpc/android.");
408+
}
409+
}
410+
}
385411

386412
#if NET5_0
387413
handler = HttpHandlerFactory.EnsureTelemetryHandler(handler);

src/Grpc.Net.Client/Internal/OperatingSystem.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,23 @@ namespace Grpc.Net.Client.Internal;
2323
internal interface IOperatingSystem
2424
{
2525
bool IsBrowser { get; }
26+
bool IsAndroid { get; }
2627
}
2728

28-
internal class OperatingSystem : IOperatingSystem
29+
internal sealed class OperatingSystem : IOperatingSystem
2930
{
3031
public static readonly OperatingSystem Instance = new OperatingSystem();
3132

3233
public bool IsBrowser { get; }
34+
public bool IsAndroid { get; }
3335

3436
private OperatingSystem()
3537
{
3638
IsBrowser = RuntimeInformation.IsOSPlatform(OSPlatform.Create("browser"));
39+
#if NET5_0_OR_GREATER
40+
IsAndroid = System.OperatingSystem.IsAndroid();
41+
#else
42+
IsAndroid = false;
43+
#endif
3744
}
3845
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#region Copyright notice and license
2+
3+
// Copyright 2019 The gRPC Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
#endregion
18+
19+
20+
namespace Grpc.Net.Client.Internal;
21+
22+
internal static class RuntimeHelpers
23+
{
24+
public static bool QueryRuntimeSettingSwitch(string switchName, bool defaultValue)
25+
{
26+
if (AppContext.TryGetSwitch(switchName, out var value))
27+
{
28+
return value;
29+
}
30+
31+
return defaultValue;
32+
}
33+
}

test/Grpc.Net.Client.Tests/GetStatusTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ public async Task AsyncUnaryCall_MissingStatusBrowser_ThrowError()
213213
private class TestOperatingSystem : IOperatingSystem
214214
{
215215
public bool IsBrowser { get; set; }
216+
public bool IsAndroid { get; set; }
216217
}
217218

218219
[Test]

test/Grpc.Net.Client.Tests/GrpcChannelTests.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
using Microsoft.Extensions.Logging.Testing;
2727
using NUnit.Framework;
2828
using Grpc.Net.Client.Internal;
29+
using System.Net;
2930
#if SUPPORT_LOAD_BALANCING
3031
using Grpc.Net.Client.Balancer;
3132
using Grpc.Net.Client.Balancer.Internal;
@@ -401,6 +402,87 @@ public async Task Dispose_CalledWhileActiveCalls_ActiveCallsDisposed()
401402
Assert.AreEqual(0, channel.ActiveCalls.Count);
402403
}
403404

405+
[TestCase(null)]
406+
[TestCase(false)]
407+
public void HttpHandler_HttpClientHandlerOverNativeOnAndroid_ThrowError(bool useDelegatingHandlers)
408+
{
409+
// Arrange
410+
AppContext.SetSwitch("System.Net.Http.UseNativeHttpHandler", true);
411+
412+
try
413+
{
414+
var services = new ServiceCollection();
415+
services.AddSingleton<IOperatingSystem>(new TestOperatingSystem { IsAndroid = true });
416+
417+
HttpMessageHandler handler = new HttpClientHandler();
418+
if (useDelegatingHandlers)
419+
{
420+
handler = new TestDelegatingHandler(handler);
421+
}
422+
423+
var ex = Assert.Throws<InvalidOperationException>(() =>
424+
{
425+
GrpcChannel.ForAddress("https://localhost", new GrpcChannelOptions
426+
{
427+
HttpHandler = handler,
428+
ServiceProvider = services.BuildServiceProvider()
429+
});
430+
});
431+
432+
Assert.AreEqual(ex!.Message, "The channel configuration isn't valid on Android devices. " +
433+
"The channel is configured to use HttpClientHandler and Android's native HTTP/2 library. " +
434+
"gRPC isn't fully supported by Android's native HTTP/2 library and it can cause runtime errors. " +
435+
"To fix this problem, either configure the channel to use SocketsHttpHandler, or add " +
436+
"<UseNativeHttpHandler>false</UseNativeHttpHandler> to the app's project file. " +
437+
"For more information, see https://aka.ms/aspnet/grpc/android.");
438+
}
439+
finally
440+
{
441+
// Reset switch for other tests.
442+
AppContext.SetSwitch("System.Net.Http.UseNativeHttpHandler", false);
443+
}
444+
}
445+
446+
private class TestDelegatingHandler : DelegatingHandler
447+
{
448+
public TestDelegatingHandler(HttpMessageHandler innerHandler) : base(innerHandler)
449+
{
450+
}
451+
}
452+
453+
[Test]
454+
[TestCase(null)]
455+
[TestCase(false)]
456+
public void HttpHandler_HttpClientHandlerOverSocketsOnAndroid_Success(bool? isNativeHttpHandler)
457+
{
458+
// Arrange
459+
if (isNativeHttpHandler != null)
460+
{
461+
AppContext.SetSwitch("System.Net.Http.UseNativeHttpHandler", isNativeHttpHandler.Value);
462+
}
463+
464+
var services = new ServiceCollection();
465+
services.AddSingleton<IOperatingSystem>(new TestOperatingSystem { IsAndroid = true });
466+
467+
var handler = new HttpClientHandler();
468+
469+
// Act
470+
var channel = GrpcChannel.ForAddress("https://localhost", new GrpcChannelOptions
471+
{
472+
HttpHandler = handler,
473+
ServiceProvider = services.BuildServiceProvider()
474+
});
475+
476+
// Assert
477+
Assert.IsTrue(channel.OperatingSystem.IsAndroid);
478+
}
479+
480+
private class TestOperatingSystem : IOperatingSystem
481+
{
482+
public bool IsBrowser { get; set; }
483+
public bool IsAndroid { get; set; }
484+
}
485+
404486
#if SUPPORT_LOAD_BALANCING
405487
[Test]
406488
public void Resolver_SocketHttpHandlerWithConnectCallback_Error()

0 commit comments

Comments
 (0)