Skip to content

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

@UniMichael

Description

@UniMichael

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).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions