Skip to content

Commit e46e6a5

Browse files
authored
Gracefully handle when unable to fetch access token in Eureka and Config Server (#1679)
1 parent b75cce0 commit e46e6a5

5 files changed

Lines changed: 142 additions & 25 deletions

File tree

src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ internal async Task ShutdownAsync(CancellationToken cancellationToken)
593593
/// <returns>
594594
/// The HttpRequestMessage built from the path.
595595
/// </returns>
596-
internal async Task<HttpRequestMessage> GetRequestMessageAsync(ConfigServerClientOptions optionsSnapshot, Uri requestUri,
596+
internal async Task<HttpRequestMessage> GetConfigServerRequestMessageAsync(ConfigServerClientOptions optionsSnapshot, Uri requestUri,
597597
CancellationToken cancellationToken)
598598
{
599599
var uriWithoutUserInfo = new Uri(requestUri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.UriEscaped));
@@ -641,18 +641,34 @@ internal async Task<HttpRequestMessage> GetRequestMessageAsync(ConfigServerClien
641641

642642
foreach (Uri requestUri in requestUris)
643643
{
644-
// Make Config Server URI from settings
645-
Uri uri = BuildConfigServerUri(optionsSnapshot, requestUri, label);
644+
try
645+
{
646+
// Make Config Server URI from settings
647+
Uri uri = BuildConfigServerUri(optionsSnapshot, requestUri, label);
648+
649+
LogTryingToConnect(uri.ToMaskedString());
650+
HttpRequestMessage request;
651+
652+
try
653+
{
654+
// Get the request message (potentially fetches access token)
655+
LogBuildingHttpRequest();
656+
request = await GetConfigServerRequestMessageAsync(optionsSnapshot, uri, cancellationToken);
657+
}
658+
catch (Exception exception) when (!exception.IsCancellation())
659+
{
660+
if (!string.IsNullOrEmpty(optionsSnapshot.AccessTokenUri))
661+
{
662+
var accessTokenUri = new Uri(optionsSnapshot.AccessTokenUri);
663+
LogFailedToFetchAccessToken(exception, accessTokenUri.ToMaskedString());
646664

647-
LogTryingToConnect(uri.ToMaskedString());
665+
continue;
666+
}
648667

649-
// Get the request message
650-
LogBuildingHttpRequest();
651-
HttpRequestMessage request = await GetRequestMessageAsync(optionsSnapshot, uri, cancellationToken);
668+
throw;
669+
}
652670

653-
// Invoke Config Server
654-
try
655-
{
671+
// Invoke Config Server
656672
LogSendingHttpRequest();
657673
using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken);
658674

@@ -808,7 +824,7 @@ internal async Task RefreshVaultTokenAsync(ConfigServerClientOptions optionsSnap
808824
{
809825
using HttpClient httpClient = CreateHttpClient(optionsSnapshot);
810826

811-
Uri uri = GetVaultRenewUri(optionsSnapshot);
827+
Uri uri = BuildVaultRenewUri(optionsSnapshot);
812828
HttpRequestMessage message = await GetVaultRenewRequestMessageAsync(optionsSnapshot, uri, cancellationToken);
813829

814830
LogRenewingVaultToken(obscuredToken, optionsSnapshot.TokenTtl, uri.ToMaskedString());
@@ -825,7 +841,7 @@ internal async Task RefreshVaultTokenAsync(ConfigServerClientOptions optionsSnap
825841
}
826842
}
827843

828-
private static Uri GetVaultRenewUri(ConfigServerClientOptions optionsSnapshot)
844+
private static Uri BuildVaultRenewUri(ConfigServerClientOptions optionsSnapshot)
829845
{
830846
string baseUri = optionsSnapshot.Uri!.Split(',')[0].Trim();
831847

@@ -1022,6 +1038,9 @@ private void ShutdownTimers()
10221038
[LoggerMessage(Level = LogLevel.Debug, Message = "Fetched access token from {AccessTokenUri}.")]
10231039
private partial void LogAccessTokenFetched(string accessTokenUri);
10241040

1041+
[LoggerMessage(Level = LogLevel.Warning, Message = "Failed to fetch access token from '{AccessTokenUri}'.")]
1042+
private partial void LogFailedToFetchAccessToken(Exception exception, string accessTokenUri);
1043+
10251044
[LoggerMessage(Level = LogLevel.Trace, Message = "Entered {Method}.")]
10261045
private partial void LogRemoteLoadEntered(string method);
10271046

src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Text;
99
using FluentAssertions.Extensions;
1010
using Microsoft.Extensions.Configuration;
11+
using Microsoft.Extensions.Logging;
1112
using Microsoft.Extensions.Logging.Abstractions;
1213
using RichardSzalay.MockHttp;
1314
using Steeltoe.Common.TestResources;
@@ -448,6 +449,38 @@ public void Load_MultipleConfigServers_SocketError_FallsBackToNextServer()
448449
value.Should().Be("value1");
449450
}
450451

452+
[Fact]
453+
public void Load_MultipleConfigServers_SocketErrorFromAccessTokenUri_LogsWarnings()
454+
{
455+
using var loggerProvider = new CapturingLoggerProvider((_, level) => level == LogLevel.Warning);
456+
using var loggerFactory = new LoggerFactory([loggerProvider]);
457+
458+
using var handler = new DelegateToMockHttpClientHandler();
459+
460+
handler.Mock.When(HttpMethod.Get, "http://auth-server.com")
461+
.Throw(new HttpRequestException("Connection refused", new SocketException((int)SocketError.ConnectionRefused)));
462+
463+
var options = new ConfigServerClientOptions
464+
{
465+
Name = "myName",
466+
AccessTokenUri = "http://auth-server.com",
467+
Uri = "http://config-server1:8888,http://config-server2:8888"
468+
};
469+
470+
using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, loggerFactory);
471+
provider.Load();
472+
473+
IList<string> logMessages = loggerProvider.GetAll();
474+
475+
logMessages.Should().BeEquivalentTo([
476+
$"WARN {typeof(ConfigServerConfigurationProvider)}: Failed to fetch access token from 'http://auth-server.com/'.",
477+
$"WARN {typeof(ConfigServerConfigurationProvider)}: Failed to fetch access token from 'http://auth-server.com/'.",
478+
$"WARN {typeof(ConfigServerConfigurationProvider)}: Failed fetching remote configuration from server(s)."
479+
], assertionOptions => assertionOptions.WithStrictOrdering());
480+
481+
provider.InnerData.Should().BeEmpty();
482+
}
483+
451484
[Fact]
452485
public void Load_IdenticalData_DoesNotTriggerReload()
453486
{

src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,9 @@ public async Task GetRequestMessage_AddsBasicAuthIfUserNameAndPasswordInURL()
152152
provider.Load();
153153

154154
Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null);
155-
HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
155+
156+
HttpRequestMessage request =
157+
await provider.GetConfigServerRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
156158

157159
request.Method.Should().Be(HttpMethod.Get);
158160
request.RequestUri.Should().Be(requestUri);
@@ -177,7 +179,9 @@ public async Task GetRequestMessage_AddsBasicAuthIfUserNameAndPasswordInSettings
177179
provider.Load();
178180

179181
Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null);
180-
HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
182+
183+
HttpRequestMessage request =
184+
await provider.GetConfigServerRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
181185

182186
request.Method.Should().Be(HttpMethod.Get);
183187
request.RequestUri.Should().Be(requestUri);
@@ -202,7 +206,9 @@ public async Task GetRequestMessage_BasicAuthInSettingsOverridesUserNameAndPassw
202206
provider.Load();
203207

204208
Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null);
205-
HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
209+
210+
HttpRequestMessage request =
211+
await provider.GetConfigServerRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
206212

207213
request.Method.Should().Be(HttpMethod.Get);
208214
request.RequestUri.Should().Be(requestUri);
@@ -225,7 +231,9 @@ public async Task GetRequestMessage_AddsVaultToken_IfNeeded()
225231
provider.Load();
226232

227233
Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), null);
228-
HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
234+
235+
HttpRequestMessage request =
236+
await provider.GetConfigServerRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
229237

230238
request.Method.Should().Be(HttpMethod.Get);
231239
request.RequestUri.Should().Be(requestUri);
@@ -260,7 +268,9 @@ public async Task GetRequestMessage_AddsBearerToken_WhenAccessTokenUriIsSet()
260268
using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance);
261269

262270
Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null);
263-
HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
271+
272+
HttpRequestMessage request =
273+
await provider.GetConfigServerRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken);
264274

265275
handler.Mock.VerifyNoOutstandingExpectation();
266276

src/Discovery/src/Eureka/EurekaClient.cs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,24 @@ private async Task<TResult> ExecuteRequestAsync<TResult>(HttpMethod method, stri
236236
Uri requestUri = GetRequestUri(serviceUri, path, queryString);
237237

238238
HttpContent? requestContent = requestBody != null ? new StringContent(requestBody, Encoding.UTF8, MediaType) : null;
239-
HttpRequestMessage request = await GetRequestMessageAsync(method, requestUri, requestContent, cancellationToken);
239+
HttpRequestMessage request;
240+
241+
try
242+
{
243+
request = await GetRequestMessageAsync(clientOptions, method, requestUri, requestContent, cancellationToken);
244+
}
245+
catch (Exception exception) when (!exception.IsCancellation())
246+
{
247+
if (!string.IsNullOrEmpty(clientOptions.AccessTokenUri))
248+
{
249+
var accessTokenUri = new Uri(clientOptions.AccessTokenUri);
250+
LogFailedToFetchAccessToken(exception, accessTokenUri.ToMaskedString(), attempt);
251+
252+
continue;
253+
}
254+
255+
throw;
256+
}
240257

241258
if (!string.IsNullOrEmpty(requestBody))
242259
{
@@ -306,7 +323,8 @@ private static Uri GetRequestUri(Uri baseUri, string path, IDictionary<string, s
306323
return requestUri;
307324
}
308325

309-
private async Task<HttpRequestMessage> GetRequestMessageAsync(HttpMethod method, Uri requestUri, HttpContent? content, CancellationToken cancellationToken)
326+
private async Task<HttpRequestMessage> GetRequestMessageAsync(EurekaClientOptions optionsSnapshot, HttpMethod method, Uri requestUri, HttpContent? content,
327+
CancellationToken cancellationToken)
310328
{
311329
var uriWithoutUserInfo = new Uri(requestUri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.UriEscaped));
312330
var requestMessage = new HttpRequestMessage(method, uriWithoutUserInfo);
@@ -320,15 +338,13 @@ private async Task<HttpRequestMessage> GetRequestMessageAsync(HttpMethod method,
320338
}
321339
else
322340
{
323-
EurekaClientOptions clientOptions = _optionsMonitor.CurrentValue;
324-
325-
if (!string.IsNullOrEmpty(clientOptions.AccessTokenUri))
341+
if (!string.IsNullOrEmpty(optionsSnapshot.AccessTokenUri))
326342
{
327343
using HttpClient httpClient = CreateHttpClient("AccessTokenForEureka", GetAccessTokenTimeout);
328-
var accessTokenUri = new Uri(clientOptions.AccessTokenUri);
344+
var accessTokenUri = new Uri(optionsSnapshot.AccessTokenUri);
329345

330-
string accessToken = await httpClient.GetAccessTokenAsync(accessTokenUri, clientOptions.ClientId,
331-
clientOptions.ClientSecret, cancellationToken);
346+
string accessToken =
347+
await httpClient.GetAccessTokenAsync(accessTokenUri, optionsSnapshot.ClientId, optionsSnapshot.ClientSecret, cancellationToken);
332348

333349
LogAccessTokenFetched(accessTokenUri.ToMaskedString());
334350
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
@@ -371,4 +387,7 @@ private async Task<HttpRequestMessage> GetRequestMessageAsync(HttpMethod method,
371387

372388
[LoggerMessage(Level = LogLevel.Debug, Message = "Fetched access token from '{AccessTokenUri}'.")]
373389
private partial void LogAccessTokenFetched(string accessTokenUri);
390+
391+
[LoggerMessage(Level = LogLevel.Warning, Message = "Failed to fetch access token from '{AccessTokenUri}' in attempt {Attempt}.")]
392+
private partial void LogFailedToFetchAccessToken(Exception exception, string accessTokenUri, int attempt);
374393
}

src/Discovery/test/Eureka.Test/Transport/EurekaClientTest.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,42 @@ public async Task RegisterAsync_ThrowsOnUnreachableServer()
194194
$"WARN {typeof(EurekaClient)}: Failed to execute HTTP POST request to 'http://host-that-does-not-exist.net:9999/apps/FOOBAR' in attempt 1.");
195195
}
196196

197+
[Fact]
198+
public async Task RegisterAsync_ThrowsOnUnreachableAccessTokenServer()
199+
{
200+
using var capturingLoggerProvider = new CapturingLoggerProvider(category => category.StartsWith("Steeltoe.", StringComparison.Ordinal));
201+
202+
var services = new ServiceCollection();
203+
services.AddLogging(options => options.SetMinimumLevel(LogLevel.Trace).AddProvider(capturingLoggerProvider));
204+
services.AddOptions<EurekaClientOptions>().Configure(options => options.AccessTokenUri = "http://host-that-does-not-exist.net:9999/");
205+
services.AddSingleton<IHttpClientFactory>(new TestHttpClientFactory());
206+
services.AddSingleton<EurekaServiceUriStateManager>();
207+
services.AddSingleton<EurekaClient>();
208+
services.AddSingleton(TimeProvider.System);
209+
210+
await using ServiceProvider serviceProvider = services.BuildServiceProvider(true);
211+
var client = serviceProvider.GetRequiredService<EurekaClient>();
212+
213+
var instance = new InstanceInfo("some", "FOOBAR", "localhost", "127.0.0.1", new DataCenterInfo(), TimeProvider.System)
214+
{
215+
NonSecurePort = 8080,
216+
IsNonSecurePortEnabled = true,
217+
SecurePort = 9090,
218+
IsSecurePortEnabled = false,
219+
LastUpdatedTimeUtc = new DateTime(638_440_245_328_236_418, DateTimeKind.Utc),
220+
LastDirtyTimeUtc = new DateTime(638_440_245_328_236_418, DateTimeKind.Utc)
221+
};
222+
223+
Func<Task> asyncAction = async () => await client.RegisterAsync(instance, TestContext.Current.CancellationToken);
224+
225+
await asyncAction.Should().ThrowExactlyAsync<EurekaTransportException>().WithMessage("Failed to execute request on all known Eureka servers.");
226+
227+
IList<string> logMessages = capturingLoggerProvider.GetAll();
228+
229+
logMessages.Should().BeEquivalentTo(
230+
$"WARN {typeof(EurekaClient)}: Failed to fetch access token from 'http://host-that-does-not-exist.net:9999/' in attempt 1.");
231+
}
232+
197233
[Fact]
198234
public async Task RegisterAsync_ThrowsOnErrorResponse()
199235
{

0 commit comments

Comments
 (0)