diff --git a/src/Grpc.Net.Client.Web/GrpcWebHandler.cs b/src/Grpc.Net.Client.Web/GrpcWebHandler.cs index d56abda11..24f2e6a6d 100644 --- a/src/Grpc.Net.Client.Web/GrpcWebHandler.cs +++ b/src/Grpc.Net.Client.Web/GrpcWebHandler.cs @@ -20,6 +20,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Grpc.Net.Client.Web.Internal; @@ -39,6 +40,9 @@ public sealed class GrpcWebHandler : DelegatingHandler { internal const string WebAssemblyEnableStreamingResponseKey = "WebAssemblyEnableStreamingResponse"; + // Internal and mutable for unit testing. + internal IOperatingSystem OperatingSystem { get; set; } = Internal.OperatingSystem.Instance; + /// /// Gets or sets the HTTP version to use when making gRPC-Web calls. /// @@ -117,6 +121,11 @@ private async Task SendAsyncCore(HttpRequestMessage request { request.Content = new GrpcWebRequestContent(request.Content!, GrpcWebMode); + if (OperatingSystem.IsBrowser) + { + FixBrowserUserAgent(request); + } + // Set WebAssemblyEnableStreamingResponse to true on gRPC-Web request. // https://github.com/mono/mono/blob/a0d69a4e876834412ba676f544d447ec331e7c01/sdks/wasm/framework/src/System.Net.Http.WebAssemblyHttpHandler/WebAssemblyHttpHandler.cs#L149 // @@ -163,14 +172,29 @@ private async Task SendAsyncCore(HttpRequestMessage request return response; } + private void FixBrowserUserAgent(HttpRequestMessage request) + { + const string userAgentHeader = "User-Agent"; + + // Remove the user-agent header and re-add it as x-user-agent. + // We don't want to override the browser's user-agent value. + // Consistent with grpc-web JS client which sends its header in x-user-agent. + // https://github.com/grpc/grpc-web/blob/2e3e8d2c501c4ddce5406ac24a637003eabae4cf/javascript/net/grpc/web/grpcwebclientbase.js#L323 + if (request.Headers.TryGetValues(userAgentHeader, out var values)) + { + request.Headers.Remove(userAgentHeader); + request.Headers.TryAddWithoutValidation("X-User-Agent", values); + } + } + private static bool IsMatchingResponseContentType(GrpcWebMode mode, string? contentType) { - if (mode == Web.GrpcWebMode.GrpcWeb) + if (mode == GrpcWebMode.GrpcWeb) { return CommonGrpcProtocolHelpers.IsContentType(GrpcWebProtocolConstants.GrpcWebContentType, contentType); } - if (mode == Web.GrpcWebMode.GrpcWebText) + if (mode == GrpcWebMode.GrpcWebText) { return CommonGrpcProtocolHelpers.IsContentType(GrpcWebProtocolConstants.GrpcWebTextContentType, contentType); } diff --git a/src/Grpc.Net.Client.Web/Internal/OperatingSystem.cs b/src/Grpc.Net.Client.Web/Internal/OperatingSystem.cs new file mode 100644 index 000000000..bf44ffb0a --- /dev/null +++ b/src/Grpc.Net.Client.Web/Internal/OperatingSystem.cs @@ -0,0 +1,39 @@ +#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 + +using System.Runtime.InteropServices; + +namespace Grpc.Net.Client.Web.Internal +{ + internal interface IOperatingSystem + { + bool IsBrowser { get; } + } + + internal class OperatingSystem : IOperatingSystem + { + public static readonly OperatingSystem Instance = new OperatingSystem(); + + public bool IsBrowser { get; } + + private OperatingSystem() + { + IsBrowser = RuntimeInformation.IsOSPlatform(OSPlatform.Create("browser")); + } + } +} diff --git a/test/Grpc.Net.Client.Tests/Web/GrpcWebHandlerTests.cs b/test/Grpc.Net.Client.Tests/Web/GrpcWebHandlerTests.cs index adc8e1eb2..4eace2677 100644 --- a/test/Grpc.Net.Client.Tests/Web/GrpcWebHandlerTests.cs +++ b/test/Grpc.Net.Client.Tests/Web/GrpcWebHandlerTests.cs @@ -18,6 +18,7 @@ using System; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -110,6 +111,36 @@ public async Task SendAsync_GrpcCall_ResponseStreamingPropertySet() Assert.AreEqual(true, testHttpHandler.WebAssemblyEnableStreamingResponse); } + [Test] + public async Task SendAsync_GrpcCallInBrowser_UserAgentFixed() + { + // Arrange + var request = new HttpRequestMessage + { + Version = HttpVersion.Version20, + Content = new ByteArrayContent(Array.Empty()) + { + Headers = { ContentType = new MediaTypeHeaderValue("application/grpc") } + } + }; + request.Headers.TryAddWithoutValidation("User-Agent", "TestUserAgent"); + var testHttpHandler = new TestHttpHandler(); + var grpcWebHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, testHttpHandler); + grpcWebHandler.OperatingSystem = new TestOperatingSystem + { + IsBrowser = true + }; + var messageInvoker = new HttpMessageInvoker(grpcWebHandler); + + // Act + await messageInvoker.SendAsync(request, CancellationToken.None); + + // Assert + Assert.AreEqual(false, testHttpHandler.RequestHeaders!.TryGetValues("user-agent", out _)); + Assert.AreEqual(true, testHttpHandler.RequestHeaders!.TryGetValues("x-user-agent", out var values)); + Assert.AreEqual("TestUserAgent", values!.Single()); + } + [Test] public async Task SendAsync_NonGrpcCall_ResponseStreamingPropertyNotSet() { @@ -133,14 +164,21 @@ public async Task SendAsync_NonGrpcCall_ResponseStreamingPropertyNotSet() Assert.AreEqual(null, testHttpHandler.WebAssemblyEnableStreamingResponse); } + private class TestOperatingSystem : IOperatingSystem + { + public bool IsBrowser { get; set; } + } + private class TestHttpHandler : HttpMessageHandler { public Version? RequestVersion { get; private set; } public bool? WebAssemblyEnableStreamingResponse { get; private set; } + public HttpRequestHeaders? RequestHeaders { get; private set; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { RequestVersion = request.Version; + RequestHeaders = request.Headers; #pragma warning disable CS0618 // Type or member is obsolete if (request.Properties.TryGetValue(GrpcWebHandler.WebAssemblyEnableStreamingResponseKey, out var enableStreaming)) #pragma warning restore CS0618 // Type or member is obsolete