Skip to content

[BUG][CSHARP][GENERICHOST] Multiple ApiKeyTokens override each other in RateLimitProvider? #21316

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
5 of 6 tasks
UniMichael opened this issue May 22, 2025 · 3 comments · Fixed by #21326
Closed
5 of 6 tasks

Comments

@UniMichael
Copy link

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

I believe I have found a bug in the C# generic host generator's RateLimitProvider implementation.

I have an API that requires 2 headers:

  • ClientId
  • Authorization

Following the instructions in the generated README.md file, I configure my API as such:

builder.Host.ConfigureApi(
    (context, services, options) =>
    {
        var config = context.Configuration.GetSection("[REDACTED]");
        var baseAddress = config.GetValue<string>("BaseAddress", string.Empty);
        var apiKey = config.GetValue<string>("ApiKey", string.Empty);
        var clientId = config.GetValue<string>("ClientId", string.Empty);
        
        options.AddTokens([
            // "prefix: string.Empty" removes the "Bearer " prefix (The API doesn't support it).
            new ApiKeyToken(clientId, ClientUtils.ApiKeyHeader.ClientId, prefix: string.Empty),
            new ApiKeyToken(apiKey, ClientUtils.ApiKeyHeader.Authorization, prefix: string.Empty),
        ]);

        options.UseProvider<RateLimitProvider<ApiKeyToken>, ApiKeyToken>();

        options.AddApiHttpClients(
            httpClient =>
            {
                httpClient.BaseAddress = new Uri(baseAddress);
            },
            httpClientBuilder =>
                httpClientBuilder
                    .AddRetryPolicy(2)
                    .AddTimeoutPolicy(TimeSpan.FromSeconds(5))
                    .AddCircuitBreakerPolicy(10, TimeSpan.FromSeconds(30))
        );
    }
);

When making calls to the API in a for loop, I've run into an issue where the API will either:

  • Return a 401, stating that my Authorization header was missing.
  • Throw an exception, stating that the Authorization header only supports 1 value.

The above is inconsistent, which led me suspect a threading or timing issue (the default token provider is the RateLimitProvider, after all).

I eventually found this code block inside the RateLimitProvider class:

foreach(global::System.Threading.Channels.Channel<TTokenBase> tokens in AvailableTokens.Values)
    for (int i = 0; i < _tokens.Length; i++)
        _tokens[i].TokenBecameAvailable += ((sender) => tokens.Writer.TryWrite((TTokenBase) sender));

A quick breakdown of the problem:

  • _tokens is an array of ApiKeyToken (handled by the base TokenProvider class). In my case: ClientId and Authorization.
  • AvailableTokens is a Dictionary<string, Channel<ApiKeyToken>>, which becomes: {"ClientId": Channel(ApiKeyToken), "Authorization": Channel(ApiKeyToken)}
  • However, the foreach loop goes over every value in AvailableTokens and then the for loop goes over every value in _tokens, so you end up overwriting all the tokens with whichever token happens to be the last one.
  • Sometimes (it's timing-based), it will fail by trying to add Authorization twice. Other times, it will fail by creating an invalid ClientId, which will have the same value twice.
openapi-generator version

openapi-generator-cli 7.14.0-SNAPSHOT
commit : 65c3126
built : -999999999-01-01T00:00:00+18:00
source : https://github.com/openapitools/openapi-generator
docs : https://openapi-generator.tech/

Generation Details

Generator:

  • csharp

Additional properties:

  • packageName=[REDACTED]
  • targetFramework=net9.0
  • nullableReferenceTypes=true
  • useDateTimeOffset=true
  • useSourceGeneration=true
  • netCoreProjectFile=true
  • apiName=[REDACTED]
  • equatable=false
Steps to reproduce
  • Generated a client with the openapi-generator-cli tool (using the above generation arguments)
  • Provide multiple tokens when configuring (following the generated README.md file):
options.AddTokens([
      new ApiKeyToken(clientId, ClientUtils.ApiKeyHeader.ClientId),
      new ApiKeyToken(apiKey, ClientUtils.ApiKeyHeader.Authorization),
  ]);
  • Make multiple calls to the API inside a for loop.
Suggest a fix

When adding the TokenBecameAvailable handler, make sure the token that is being written to matches the one doing the writing (consider modifying the for loop instead of iterating over all the values in _tokens).

@devhl-labs
Copy link
Contributor

devhl-labs commented May 24, 2025

This is an excellent write up, thanks for that. The section of code you suspect is in the constructor of a class which is a singleton, so it can't really be the issue. I think the section you're interested in is the GetAsync method in the rate limit provider. It should always retrieve the correct token as long as the header parameter was provided, so I'm not really sure what is wrong here. If you figure it out, you can provide your own class to work around the issue now, but I hope we can send a PR once we identify the issue.

I think I see what you mean...

@devhl-labs
Copy link
Contributor

Please give this a try #21326

@UniMichael
Copy link
Author

Hey there. Sorry for the late reply. I've been testing it and can confirm this fixes the issue. Thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants