Skip to content

Commit 84a4fcb

Browse files
authored
Support some HttpClientAction settings and log debug message instead of an error (#2448)
1 parent bca6485 commit 84a4fcb

File tree

3 files changed

+154
-8
lines changed

3 files changed

+154
-8
lines changed

src/Grpc.Net.ClientFactory/Internal/CallOptionsConfigurationInvoker.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#region Copyright notice and license
1+
#region Copyright notice and license
22

33
// Copyright 2019 The gRPC Authors
44
//
@@ -33,6 +33,9 @@ public CallOptionsConfigurationInvoker(CallInvoker innerInvoker, IList<Action<Ca
3333
_serviceProvider = serviceProvider;
3434
}
3535

36+
// Internal for testing.
37+
internal CallInvoker InnerInvoker => _innerInvoker;
38+
3639
private CallOptions ResolveCallOptions(CallOptions callOptions)
3740
{
3841
var context = new CallOptionsContext(callOptions, _serviceProvider);

src/Grpc.Net.ClientFactory/Internal/GrpcCallInvokerFactory.cs

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
#endregion
1818

1919
using System.Collections.Concurrent;
20+
using System.Globalization;
21+
using System.Net.Http.Headers;
2022
using Grpc.Core;
2123
using Grpc.Net.Client;
2224
using Grpc.Shared;
@@ -39,6 +41,7 @@ internal class GrpcCallInvokerFactory
3941
private readonly IServiceScopeFactory _scopeFactory;
4042
private readonly ConcurrentDictionary<EntryKey, CallInvoker> _activeChannels;
4143
private readonly Func<EntryKey, CallInvoker> _invokerFactory;
44+
private readonly ILogger<GrpcCallInvokerFactory> _logger;
4245

4346
public GrpcCallInvokerFactory(
4447
IServiceScopeFactory scopeFactory,
@@ -57,6 +60,7 @@ public GrpcCallInvokerFactory(
5760
_scopeFactory = scopeFactory;
5861
_activeChannels = new ConcurrentDictionary<EntryKey, CallInvoker>();
5962
_invokerFactory = CreateInvoker;
63+
_logger = _loggerFactory.CreateLogger<GrpcCallInvokerFactory>();
6064
}
6165

6266
public CallInvoker CreateInvoker(string name, Type type)
@@ -73,12 +77,58 @@ private CallInvoker CreateInvoker(EntryKey key)
7377
try
7478
{
7579
var httpClientFactoryOptions = _httpClientFactoryOptionsMonitor.Get(name);
80+
var clientFactoryOptions = _grpcClientFactoryOptionsMonitor.Get(name);
81+
82+
// gRPC channel is configured with a handler instead of a client, so HttpClientActions aren't used directly.
83+
// To capture HttpClient configuration, a temp HttpClient is created and configured using HttpClientActions.
84+
// Values from the temp HttpClient are then copied to the gRPC channel.
85+
// Only values with overlap on both types are copied so log a message about the limitations.
7686
if (httpClientFactoryOptions.HttpClientActions.Count > 0)
7787
{
78-
throw new InvalidOperationException($"The ConfigureHttpClient method is not supported when creating gRPC clients. Unable to create client with name '{name}'.");
88+
Log.HttpClientActionsPartiallySupported(_logger, name);
89+
90+
var httpClient = new HttpClient(NullHttpMessageHandler.Instance);
91+
foreach (var applyOptions in httpClientFactoryOptions.HttpClientActions)
92+
{
93+
applyOptions(httpClient);
94+
}
95+
96+
// Copy configuration from HttpClient to GrpcChannel/CallOptions.
97+
// This configuration should be overriden by gRPC specific config methods.
98+
if (clientFactoryOptions.Address == null)
99+
{
100+
clientFactoryOptions.Address = httpClient.BaseAddress;
101+
}
102+
103+
if (httpClient.DefaultRequestHeaders.Any())
104+
{
105+
var defaultHeaders = httpClient.DefaultRequestHeaders.ToList();
106+
107+
// Merge DefaultRequestHeaders with CallOptions.Headers.
108+
// Follow behavior of DefaultRequestHeaders on HttpClient when merging.
109+
// Don't replace or add new header values if the header name has already been set.
110+
clientFactoryOptions.CallOptionsActions.Add(callOptionsContext =>
111+
{
112+
var metadata = callOptionsContext.CallOptions.Headers ?? new Metadata();
113+
foreach (var entry in defaultHeaders)
114+
{
115+
// grpc requires header names are lower case before being added to collection.
116+
var resolvedKey = entry.Key.ToLower(CultureInfo.InvariantCulture);
117+
118+
if (metadata.Get(resolvedKey) == null)
119+
{
120+
foreach (var value in entry.Value)
121+
{
122+
metadata.Add(resolvedKey, value);
123+
}
124+
}
125+
}
126+
127+
callOptionsContext.CallOptions = callOptionsContext.CallOptions.WithHeaders(metadata);
128+
});
129+
}
79130
}
80131

81-
var clientFactoryOptions = _grpcClientFactoryOptionsMonitor.Get(name);
82132
var httpHandler = _messageHandlerFactory.CreateHandler(name);
83133
if (httpHandler == null)
84134
{
@@ -190,4 +240,25 @@ public override void SetCompositeCredentials(object state, ChannelCredentials ch
190240
{
191241
}
192242
}
243+
244+
private sealed class NullHttpMessageHandler : HttpMessageHandler
245+
{
246+
public static readonly NullHttpMessageHandler Instance = new NullHttpMessageHandler();
247+
248+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
249+
{
250+
throw new NotImplementedException();
251+
}
252+
}
253+
254+
private static class Log
255+
{
256+
private static readonly Action<ILogger, string, Exception?> _httpClientActionsPartiallySupported =
257+
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(1, "HttpClientActionsPartiallySupported"), "The ConfigureHttpClient method is used to configure gRPC client '{ClientName}'. ConfigureHttpClient is partially supported when creating gRPC clients and only some HttpClient properties such as BaseAddress and DefaultRequestHeaders are applied to the gRPC client.");
258+
259+
public static void HttpClientActionsPartiallySupported(ILogger<GrpcCallInvokerFactory> logger, string clientName)
260+
{
261+
_httpClientActionsPartiallySupported(logger, clientName, null);
262+
}
263+
}
193264
}

test/Grpc.Net.ClientFactory.Tests/DefaultGrpcClientFactoryTests.cs

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
#endregion
1818

19+
using System.Net;
20+
using System.Net.Http.Headers;
1921
using Greet;
2022
using Grpc.Core;
2123
using Grpc.Net.Client.Internal;
@@ -144,27 +146,93 @@ public void CreateClient_NoAddress_ThrowError()
144146
}
145147

146148
[Test]
147-
public void CreateClient_ConfigureHttpClient_ThrowError()
149+
public async Task CreateClient_ConfigureHttpClient_LogMessage()
148150
{
149151
// Arrange
152+
var testSink = new TestSink();
153+
Uri? requestUri = null;
154+
HttpRequestHeaders? requestHeaders = null;
155+
150156
var services = new ServiceCollection();
157+
services.AddLogging(configure => configure.SetMinimumLevel(LogLevel.Trace));
158+
services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, TestLoggerProvider>(s => new TestLoggerProvider(testSink, true)));
151159
services
152160
.AddGrpcClient<TestGreeterClient>()
153-
.ConfigureHttpClient(options => options.BaseAddress = new Uri("http://contoso"))
161+
.ConfigureHttpClient(options =>
162+
{
163+
options.BaseAddress = new Uri("http://contoso");
164+
options.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", "abc");
165+
})
166+
.ConfigurePrimaryHttpMessageHandler(() =>
167+
{
168+
return TestHttpMessageHandler.Create(async r =>
169+
{
170+
requestUri = r.RequestUri;
171+
requestHeaders = r.Headers;
172+
173+
var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
174+
return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
175+
});
176+
});
177+
178+
var serviceProvider = services.BuildServiceProvider(validateScopes: true);
179+
180+
var clientFactory = CreateGrpcClientFactory(serviceProvider);
181+
182+
// Act
183+
var client = clientFactory.CreateClient<TestGreeterClient>(nameof(TestGreeterClient));
184+
var response = await client.SayHelloAsync(new HelloRequest()).ResponseAsync.DefaultTimeout();
185+
186+
// Assert
187+
Assert.AreEqual("http://contoso", client.CallInvoker.Channel.Address.OriginalString);
188+
Assert.AreEqual(new Uri("http://contoso/greet.Greeter/SayHello"), requestUri);
189+
Assert.AreEqual("bearer abc", requestHeaders!.GetValues("authorization").Single());
190+
191+
Assert.IsTrue(testSink.Writes.Any(w => w.EventId.Name == "HttpClientActionsPartiallySupported"));
192+
}
193+
194+
[Test]
195+
public async Task CreateClient_ConfigureHttpClient_OverridenByGrpcConfiguration()
196+
{
197+
// Arrange
198+
Uri? requestUri = null;
199+
HttpRequestHeaders? requestHeaders = null;
200+
201+
var services = new ServiceCollection();
202+
services
203+
.AddGrpcClient<TestGreeterClient>(o => o.Address = new Uri("http://eshop"))
204+
.ConfigureHttpClient(options =>
205+
{
206+
options.BaseAddress = new Uri("http://contoso");
207+
options.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", "abc");
208+
options.DefaultRequestHeaders.TryAddWithoutValidation("HTTPCLIENT-KEY", "httpclient-value");
209+
})
154210
.ConfigurePrimaryHttpMessageHandler(() =>
155211
{
156-
return new NullHttpHandler();
212+
return TestHttpMessageHandler.Create(async r =>
213+
{
214+
requestUri = r.RequestUri;
215+
requestHeaders = r.Headers;
216+
217+
var streamContent = await ClientTestHelpers.CreateResponseContent(new HelloReply()).DefaultTimeout();
218+
return ResponseUtils.CreateResponse(HttpStatusCode.OK, streamContent);
219+
});
157220
});
158221

159222
var serviceProvider = services.BuildServiceProvider(validateScopes: true);
160223

161224
var clientFactory = CreateGrpcClientFactory(serviceProvider);
162225

163226
// Act
164-
var ex = Assert.Throws<InvalidOperationException>(() => clientFactory.CreateClient<TestGreeterClient>(nameof(TestGreeterClient)))!;
227+
var client = clientFactory.CreateClient<TestGreeterClient>(nameof(TestGreeterClient));
228+
var response = await client.SayHelloAsync(new HelloRequest(), headers: new Metadata { new Metadata.Entry("authorization", "bearer 123"), new Metadata.Entry("call-key", "call-value") }).ResponseAsync.DefaultTimeout();
165229

166230
// Assert
167-
Assert.AreEqual("The ConfigureHttpClient method is not supported when creating gRPC clients. Unable to create client with name 'TestGreeterClient'.", ex.Message);
231+
Assert.AreEqual("http://eshop", client.CallInvoker.Channel.Address.OriginalString);
232+
Assert.AreEqual(new Uri("http://eshop/greet.Greeter/SayHello"), requestUri);
233+
Assert.AreEqual("bearer 123", requestHeaders!.GetValues("authorization").Single());
234+
Assert.AreEqual("httpclient-value", requestHeaders!.GetValues("httpclient-key").Single());
235+
Assert.AreEqual("call-value", requestHeaders!.GetValues("call-key").Single());
168236
}
169237

170238
#if NET462
@@ -292,6 +360,10 @@ internal class TestGreeterClient : Greeter.GreeterClient
292360
{
293361
public TestGreeterClient(CallInvoker callInvoker) : base(callInvoker)
294362
{
363+
if (callInvoker is CallOptionsConfigurationInvoker callOptionsInvoker)
364+
{
365+
callInvoker = callOptionsInvoker.InnerInvoker;
366+
}
295367
CallInvoker = (HttpClientCallInvoker)callInvoker;
296368
}
297369

0 commit comments

Comments
 (0)