Skip to content

Commit e34cef6

Browse files
authored
Fix setting user-agent in client when running in the browser (#1165)
1 parent 7d6683e commit e34cef6

File tree

3 files changed

+103
-2
lines changed

3 files changed

+103
-2
lines changed

src/Grpc.Net.Client.Web/GrpcWebHandler.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using System.Net;
2121
using System.Net.Http;
2222
using System.Net.Http.Headers;
23+
using System.Runtime.InteropServices;
2324
using System.Threading;
2425
using System.Threading.Tasks;
2526
using Grpc.Net.Client.Web.Internal;
@@ -39,6 +40,9 @@ public sealed class GrpcWebHandler : DelegatingHandler
3940
{
4041
internal const string WebAssemblyEnableStreamingResponseKey = "WebAssemblyEnableStreamingResponse";
4142

43+
// Internal and mutable for unit testing.
44+
internal IOperatingSystem OperatingSystem { get; set; } = Internal.OperatingSystem.Instance;
45+
4246
/// <summary>
4347
/// Gets or sets the HTTP version to use when making gRPC-Web calls.
4448
/// <para>
@@ -117,6 +121,11 @@ private async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request
117121
{
118122
request.Content = new GrpcWebRequestContent(request.Content!, GrpcWebMode);
119123

124+
if (OperatingSystem.IsBrowser)
125+
{
126+
FixBrowserUserAgent(request);
127+
}
128+
120129
// Set WebAssemblyEnableStreamingResponse to true on gRPC-Web request.
121130
// https://github.com/mono/mono/blob/a0d69a4e876834412ba676f544d447ec331e7c01/sdks/wasm/framework/src/System.Net.Http.WebAssemblyHttpHandler/WebAssemblyHttpHandler.cs#L149
122131
//
@@ -163,14 +172,29 @@ private async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request
163172
return response;
164173
}
165174

175+
private void FixBrowserUserAgent(HttpRequestMessage request)
176+
{
177+
const string userAgentHeader = "User-Agent";
178+
179+
// Remove the user-agent header and re-add it as x-user-agent.
180+
// We don't want to override the browser's user-agent value.
181+
// Consistent with grpc-web JS client which sends its header in x-user-agent.
182+
// https://github.com/grpc/grpc-web/blob/2e3e8d2c501c4ddce5406ac24a637003eabae4cf/javascript/net/grpc/web/grpcwebclientbase.js#L323
183+
if (request.Headers.TryGetValues(userAgentHeader, out var values))
184+
{
185+
request.Headers.Remove(userAgentHeader);
186+
request.Headers.TryAddWithoutValidation("X-User-Agent", values);
187+
}
188+
}
189+
166190
private static bool IsMatchingResponseContentType(GrpcWebMode mode, string? contentType)
167191
{
168-
if (mode == Web.GrpcWebMode.GrpcWeb)
192+
if (mode == GrpcWebMode.GrpcWeb)
169193
{
170194
return CommonGrpcProtocolHelpers.IsContentType(GrpcWebProtocolConstants.GrpcWebContentType, contentType);
171195
}
172196

173-
if (mode == Web.GrpcWebMode.GrpcWebText)
197+
if (mode == GrpcWebMode.GrpcWebText)
174198
{
175199
return CommonGrpcProtocolHelpers.IsContentType(GrpcWebProtocolConstants.GrpcWebTextContentType, contentType);
176200
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
using System.Runtime.InteropServices;
20+
21+
namespace Grpc.Net.Client.Web.Internal
22+
{
23+
internal interface IOperatingSystem
24+
{
25+
bool IsBrowser { get; }
26+
}
27+
28+
internal class OperatingSystem : IOperatingSystem
29+
{
30+
public static readonly OperatingSystem Instance = new OperatingSystem();
31+
32+
public bool IsBrowser { get; }
33+
34+
private OperatingSystem()
35+
{
36+
IsBrowser = RuntimeInformation.IsOSPlatform(OSPlatform.Create("browser"));
37+
}
38+
}
39+
}

test/Grpc.Net.Client.Tests/Web/GrpcWebHandlerTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
using System;
2020
using System.IO;
21+
using System.Linq;
2122
using System.Net;
2223
using System.Net.Http;
2324
using System.Net.Http.Headers;
@@ -110,6 +111,36 @@ public async Task SendAsync_GrpcCall_ResponseStreamingPropertySet()
110111
Assert.AreEqual(true, testHttpHandler.WebAssemblyEnableStreamingResponse);
111112
}
112113

114+
[Test]
115+
public async Task SendAsync_GrpcCallInBrowser_UserAgentFixed()
116+
{
117+
// Arrange
118+
var request = new HttpRequestMessage
119+
{
120+
Version = HttpVersion.Version20,
121+
Content = new ByteArrayContent(Array.Empty<byte>())
122+
{
123+
Headers = { ContentType = new MediaTypeHeaderValue("application/grpc") }
124+
}
125+
};
126+
request.Headers.TryAddWithoutValidation("User-Agent", "TestUserAgent");
127+
var testHttpHandler = new TestHttpHandler();
128+
var grpcWebHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, testHttpHandler);
129+
grpcWebHandler.OperatingSystem = new TestOperatingSystem
130+
{
131+
IsBrowser = true
132+
};
133+
var messageInvoker = new HttpMessageInvoker(grpcWebHandler);
134+
135+
// Act
136+
await messageInvoker.SendAsync(request, CancellationToken.None);
137+
138+
// Assert
139+
Assert.AreEqual(false, testHttpHandler.RequestHeaders!.TryGetValues("user-agent", out _));
140+
Assert.AreEqual(true, testHttpHandler.RequestHeaders!.TryGetValues("x-user-agent", out var values));
141+
Assert.AreEqual("TestUserAgent", values!.Single());
142+
}
143+
113144
[Test]
114145
public async Task SendAsync_NonGrpcCall_ResponseStreamingPropertyNotSet()
115146
{
@@ -133,14 +164,21 @@ public async Task SendAsync_NonGrpcCall_ResponseStreamingPropertyNotSet()
133164
Assert.AreEqual(null, testHttpHandler.WebAssemblyEnableStreamingResponse);
134165
}
135166

167+
private class TestOperatingSystem : IOperatingSystem
168+
{
169+
public bool IsBrowser { get; set; }
170+
}
171+
136172
private class TestHttpHandler : HttpMessageHandler
137173
{
138174
public Version? RequestVersion { get; private set; }
139175
public bool? WebAssemblyEnableStreamingResponse { get; private set; }
176+
public HttpRequestHeaders? RequestHeaders { get; private set; }
140177

141178
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
142179
{
143180
RequestVersion = request.Version;
181+
RequestHeaders = request.Headers;
144182
#pragma warning disable CS0618 // Type or member is obsolete
145183
if (request.Properties.TryGetValue(GrpcWebHandler.WebAssemblyEnableStreamingResponseKey, out var enableStreaming))
146184
#pragma warning restore CS0618 // Type or member is obsolete

0 commit comments

Comments
 (0)