Skip to content

Commit 35b17a9

Browse files
committed
Retry requests on 401 Unauthorized result
When a token expires, the API returns 401 Unauthorized. At that point the SDK should request a new token and retry the request. The new OpenStack.HttpTest class enables unit testing this behavior. If the Flurl HttpTest class is used, then our special message handler is not used and this authentication behavior is not exercised in unit tests. Which should not be an issue, unless the user was explicitly attempting to test out the retry logic.
1 parent 72a3900 commit 35b17a9

File tree

13 files changed

+324
-70
lines changed

13 files changed

+324
-70
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Net.Http;
2+
using Flurl;
3+
using Flurl.Http;
4+
using Flurl.Http.Configuration;
5+
6+
namespace OpenStack.Authentication
7+
{
8+
/// <summary>
9+
/// Instructs Flurl to use our <see cref="AuthenticatedMessageHandler"/> for all requests.
10+
/// </summary>
11+
internal class AuthenticatedHttpClientFactory : DefaultHttpClientFactory
12+
{
13+
public override HttpClient CreateClient(Url url, HttpMessageHandler handler)
14+
{
15+
return new HttpClient(handler)
16+
{
17+
Timeout = FlurlHttp.Configuration.DefaultTimeout
18+
};
19+
}
20+
21+
public override HttpMessageHandler CreateMessageHandler()
22+
{
23+
return new AuthenticatedMessageHandler(base.CreateMessageHandler());
24+
}
25+
}
26+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Net;
2+
using System.Net.Http;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Flurl.Http;
6+
using Flurl.Http.Configuration;
7+
8+
namespace OpenStack.Authentication
9+
{
10+
/// <summary>
11+
/// Used by Flurl for all requests. Understands how to authenticate and retry when a token expires.
12+
/// </summary>
13+
internal class AuthenticatedMessageHandler : FlurlMessageHandler
14+
{
15+
public AuthenticatedMessageHandler(HttpMessageHandler innerHandler)
16+
: base(innerHandler)
17+
{ }
18+
19+
public IAuthenticationProvider AuthenticationProvider;
20+
21+
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
22+
{
23+
string token = await AuthenticationProvider.GetToken(cancellationToken).ConfigureAwait(false);
24+
request.Headers.SetAuthToken(token);
25+
26+
try
27+
{
28+
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
29+
}
30+
catch (FlurlHttpException ex)
31+
{
32+
if (ex.Call.HttpStatus != HttpStatusCode.Unauthorized)
33+
throw;
34+
}
35+
36+
// Retry with a new token
37+
var retryRequest = request.Copy();
38+
var retryToken = await AuthenticationProvider.GetToken(cancellationToken).ConfigureAwait(false);
39+
retryRequest.Headers.SetAuthToken(retryToken);
40+
return await base.SendAsync(retryRequest, cancellationToken).ConfigureAwait(false);
41+
}
42+
}
43+
}

src/corelib/Authentication/ServiceUrlBuilder.cs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System.Threading;
22
using System.Threading.Tasks;
33
using Flurl;
4-
using Flurl.Http;
54

65
namespace OpenStack.Authentication
76
{
@@ -25,16 +24,5 @@ public async Task<Url> GetEndpoint(CancellationToken cancellationToken)
2524
string endpoint = await _authenticationProvider.GetEndpoint(_serviceType, _region, _useInternalUrl, cancellationToken).ConfigureAwait(false);
2625
return new Url(endpoint);
2726
}
28-
29-
public async Task<string> GetToken(CancellationToken cancellationToken)
30-
{
31-
return await _authenticationProvider.GetToken(cancellationToken).ConfigureAwait(false);
32-
}
33-
34-
public async Task<FlurlClient> Authenticate(FlurlClient client, CancellationToken cancellationToken)
35-
{
36-
string token = await GetToken(cancellationToken).ConfigureAwait(false);
37-
return client.Authenticate(token);
38-
}
3927
}
4028
}

src/corelib/Configuration.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System;
2+
using Flurl.Http;
3+
using Flurl.Http.Configuration;
4+
using Newtonsoft.Json;
5+
using OpenStack.Authentication;
6+
using OpenStack.Serialization;
7+
8+
namespace OpenStack
9+
{
10+
/// <summary>
11+
/// A static container for global configuration settings affecting OpenStack.NET behavior.
12+
/// </summary>
13+
/// <threadsafety static="true" instance="false"/>
14+
public static class OpenStackNet
15+
{
16+
private static readonly object ConfigureLock = new object();
17+
private static bool _isConfigured = false;
18+
19+
/// <summary>
20+
/// Provides thread-safe accesss to OpenStack.NET's global configuration options.
21+
/// <para>
22+
/// Can only be called once at application start-up, before instantiating any OpenStack.NET objects.
23+
/// </para>
24+
/// </summary>
25+
/// <param name="configureFlurl">Addtional configuration of Flurl's global settings <seealso cref="Flurl.Http.FlurlHttp.Configure"/>.</param>
26+
/// <param name="configureJson">Additional configuration of Json.NET's glboal settings <seealso cref="Newtonsoft.Json.JsonConvert.DefaultSettings"/>.</param>
27+
public static void Configure(Action<FlurlHttpConfigurationOptions> configureFlurl = null, Action<JsonSerializerSettings> configureJson = null)
28+
{
29+
if (_isConfigured)
30+
return;
31+
32+
lock (ConfigureLock)
33+
{
34+
if (_isConfigured)
35+
return;
36+
37+
JsonConvert.DefaultSettings = () =>
38+
{
39+
// Apply our default settings
40+
var settings = new JsonSerializerSettings
41+
{
42+
DefaultValueHandling = DefaultValueHandling.Ignore,
43+
NullValueHandling = NullValueHandling.Ignore,
44+
ContractResolver = new EmptyEnumerableResolver()
45+
};
46+
47+
// Apply application's default settings
48+
if (configureJson != null)
49+
configureJson(settings);
50+
return settings;
51+
};
52+
53+
FlurlHttp.Configure(c =>
54+
{
55+
// Apply our default settings
56+
c.HttpClientFactory = new AuthenticatedHttpClientFactory();
57+
58+
// Apply application's default settings
59+
if (configureFlurl != null)
60+
configureFlurl(c);
61+
});
62+
63+
_isConfigured = true;
64+
}
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)