Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,22 @@ Lazy<Task<TokenIntrospectionResponse>> GetTokenIntrospectionResponseLazy(string
// "If the response contains the "exp" parameter (expiration), the response MUST NOT be cached beyond the time indicated therein."
// so we need to cache items ourselves here. There is discussion of adding this to hybrid cache:
// https://github.com/dotnet/extensions/issues/6434, https://github.com/dotnet/aspnetcore/issues/56483
var response = await IntrospectionDictionary
.GetOrAdd(token, GetTokenIntrospectionResponseLazy)
.Value;
TokenIntrospectionResponse response;
try
{
response = await IntrospectionDictionary
.GetOrAdd(token, GetTokenIntrospectionResponseLazy)
.Value;
}
catch
{
// The shared task faulted (e.g. timeout, cancellation, network error).
// Evict the faulted entry so future requests start fresh, then retry
// once with a direct call — bypassing the dictionary — so this request
// is not affected by another request's failure.
IntrospectionDictionary.TryRemove(token, out _);
Comment on lines +116 to +128
response = await LoadClaimsForToken(token, Context, Scheme, Events, Options);
}

if (response.IsError)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,114 @@ public async Task ActiveToken_WithTwoConcurrentCalls_FirstCancelled_SecondShould
result2.StatusCode.ShouldBe(HttpStatusCode.OK);
}

[Fact]
public async Task TwoConcurrentCalls_FirstIntrospectFaults_SecondShouldRetryAndSucceed()
{
const string token = "sometoken";
var waitForFirstIntrospectionToStart = new ManualResetEvent(initialState: false);
var waitForSecondRequestToStart = new ManualResetEvent(initialState: false);
var handler = new IntrospectionEndpointHandler(IntrospectionEndpointHandler.Behavior.Active);

var requestCount = 0;

var messageHandler = await PipelineFactory.CreateHandler(o =>
{
_options(o);

o.Events.OnSendingRequest = async context =>
{
var count = Interlocked.Increment(ref requestCount);

if (count == 1)
{
// Signal R2 it can proceed, then wait for R2 to be waiting on the dictionary entry
waitForSecondRequestToStart.WaitOne();
waitForFirstIntrospectionToStart.Set();
await Task.Delay(200); // wait for R2 to reach IntrospectionDictionary
throw new HttpRequestException("Simulated network error");
}
// Subsequent calls (retries) succeed normally
};
}, handler);

var client1 = new HttpClient(messageHandler);
var request1 = Task.Run(async () =>
{
client1.SetBearerToken(token);
return await client1.GetAsync("http://test");
});

var client2 = new HttpClient(messageHandler);
var request2 = Task.Run(async () =>
{
waitForSecondRequestToStart.Set();
waitForFirstIntrospectionToStart.WaitOne();
client2.SetBearerToken(token);
Comment on lines +190 to +212
return await client2.GetAsync("http://test");
});

await Task.WhenAll(request1, request2);

var result1 = await request1;
result1.StatusCode.ShouldBe(HttpStatusCode.OK);

var result2 = await request2;
result2.StatusCode.ShouldBe(HttpStatusCode.OK);
}

[Fact]
public async Task TwoConcurrentCalls_FirstIntrospectFaults_RetryAlsoFaults_ShouldPropagateException()
{
const string token = "sometoken";
var waitForFirstIntrospectionToStart = new ManualResetEvent(initialState: false);
var waitForSecondRequestToStart = new ManualResetEvent(initialState: false);
var handler = new IntrospectionEndpointHandler(IntrospectionEndpointHandler.Behavior.Active);

var requestCount = 0;

var messageHandler = await PipelineFactory.CreateHandler(o =>
{
_options(o);

o.Events.OnSendingRequest = async context =>
{
var count = Interlocked.Increment(ref requestCount);

if (count == 1)
{
// Signal R2, wait for it to reach the dictionary, then fault
waitForSecondRequestToStart.WaitOne();
waitForFirstIntrospectionToStart.Set();
await Task.Delay(200); // wait for R2 to reach IntrospectionDictionary
}
Comment on lines +245 to +249

// All calls (initial + retries) throw — simulating persistent failure
throw new HttpRequestException("Persistent network error");
};
}, handler);

var client1 = new HttpClient(messageHandler);
var request1 = Task.Run(async () =>
{
client1.SetBearerToken(token);
var doRequest = () => client1.GetAsync("http://test");
await doRequest.ShouldThrowAsync<HttpRequestException>();
});

var client2 = new HttpClient(messageHandler);
var request2 = Task.Run(async () =>
{
waitForSecondRequestToStart.Set();
waitForFirstIntrospectionToStart.WaitOne();
client2.SetBearerToken(token);
var doRequest = () => client2.GetAsync("http://test");
await doRequest.ShouldThrowAsync<HttpRequestException>();
});

// Both requests should complete (not hang) — exceptions propagate as HttpRequestException
await Task.WhenAll(request1, request2);
}

[Theory]
[InlineData(5000, "testAssertion1", "testAssertion1")]
[InlineData(-5000, "testAssertion1", "testAssertion2")]
Expand Down
Loading