You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Service Bus Topic触发器的Azure Function注册JanrainApiClient单例时遭遇重复键异常求助

问题分析

你的异常根源在于单例的JanrainApiClient持有了一个全局共享的HttpClient实例。HttpClient的BaseAddress、DefaultRequestHeaders这类配置属性并不是线程安全的,当Service Bus Topic触发多个并发的Function执行时,多个请求会同时修改这个共享HttpClient的属性:

  • 并发修改Header会导致内部字典的键冲突,抛出An item with the same key has already been added错误
  • 当HttpClient已经发起请求后再修改配置,会触发This instance has already started one or more requests异常

解决方案

这里提供两种可行的解决方式,推荐第二种更优雅的实现:

方案1:每次请求创建独立的HttpClient实例

修改JanrainApiClient,不在构造函数中保存HttpClient,而是每次调用API时从IHttpClientFactory创建新实例,确保每个请求的配置都是独立的:

public class JanrainApiClient : IJanrainApiClient {
    private readonly ILogger<JanrainApiClient> logger;
    private readonly IHttpClientFactory httpClientFactory;
    private readonly JanrainConfigOption janrainConfigOption;

    public JanrainApiClient(ILogger<JanrainApiClient> logger, IOptions<JanrainConfigOption> janrainConfigOption, IHttpClientFactory httpClientFactory) {
        this.logger = logger;
        this.janrainConfigOption = janrainConfigOption.Value;
        this.httpClientFactory = httpClientFactory; // 仅保存工厂实例
    }

    public async Task<JanrainResultDto> FindAsync(string userUUID) {
        try {
            logger.LogInformation("{class} - {method} - Start", nameof(JanrainApiClient), nameof(JanrainApiClient.FindAsync));
            JanrainResultDto resultDto = new JanrainResultDto();
            var request = GetJanrainApiRequest(userUUID);
            var serviceUrl = GetServiceUrl(request);

            // 每次请求创建新的HttpClient,确保配置独立
            using var httpClient = httpClientFactory.CreateClient();
            httpClient.BaseAddress = new Uri(serviceUrl);
            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(janrainConfigOption.Scheme, janrainConfigOption.ApiAuth);

            var response = await httpClient.GetAsync(serviceUrl);
            var jsonAsString = await response.Content.ReadAsStringAsync();
            JanrainApiResponse entityFindResponse = jsonAsString.AsPoco<JanrainApiResponse>();

            if (entityFindResponse != null && entityFindResponse.Results != null) {
                resultDto = entityFindResponse.Results.FirstOrDefault();
            } else {
                logger.LogError($"Error while requesting Janrain info for UserUUId:{userUUID}. Error:{entityFindResponse?.Error}, ErrorMessage:{entityFindResponse?.ErrorDescription}");
            }

            logger.LogInformation("{class} - {method} - End", nameof(JanrainApiClient), nameof(JanrainApiClient.FindAsync));
            return resultDto;
        } catch (Exception ex) {
            logger.LogError($"Exception while calling Janrain entity Find API for UserUUId :{userUUID}, Error message:{ex.Message}, StackTrace:{ex.StackTrace}");
            throw;
        }
    }

    // 其他方法保持不变
}

方案2:使用命名HttpClient(推荐)

通过IHttpClientFactory配置命名的HttpClient,预先设置好所有固定配置,避免每次请求重复修改属性,同时利用工厂管理HttpClient的生命周期,避免Socket资源耗尽:

第一步:修改Startup.cs配置命名HttpClient

public class Startup : FunctionsStartup {
    public override void Configure(IFunctionsHostBuilder builder) {
        builder.Services.AddLogging();
        
        // 配置专用于Janrain的命名HttpClient
        builder.Services.AddHttpClient("JanrainClient", (serviceProvider, client) => {
            var config = serviceProvider.GetRequiredService<IOptions<JanrainConfigOption>>().Value;
            // 如果Janrain的API基础地址固定,可以直接设置BaseAddress
            // client.BaseAddress = new Uri(config.EntityFindApiUrl);
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(config.Scheme, config.ApiAuth);
        });
        
        builder.Services.AddSingleton<IJanrainApiClient, JanrainApiClient>();
    }
}

第二步:修改JanrainApiClient注入命名HttpClient

public class JanrainApiClient : IJanrainApiClient {
    private readonly ILogger<JanrainApiClient> logger;
    private readonly HttpClient httpClient;
    private readonly JanrainConfigOption janrainConfigOption;

    // 注入预先配置好的命名HttpClient
    public JanrainApiClient(ILogger<JanrainApiClient> logger, IOptions<JanrainConfigOption> janrainConfigOption, IHttpClientFactory httpClientFactory) {
        this.logger = logger;
        this.janrainConfigOption = janrainConfigOption.Value;
        this.httpClient = httpClientFactory.CreateClient("JanrainClient");
    }

    public async Task<JanrainResultDto> FindAsync(string userUUID) {
        try {
            logger.LogInformation("{class} - {method} - Start", nameof(JanrainApiClient), nameof(JanrainApiClient.FindAsync));
            JanrainResultDto resultDto = new JanrainResultDto();
            var request = GetJanrainApiRequest(userUUID);
            var serviceUrl = GetServiceUrl(request);

            // 直接使用预配置的HttpClient,无需再修改任何属性
            var response = await httpClient.GetAsync(serviceUrl);
            var jsonAsString = await response.Content.ReadAsStringAsync();
            JanrainApiResponse entityFindResponse = jsonAsString.AsPoco<JanrainApiResponse>();

            if (entityFindResponse != null && entityFindResponse.Results != null) {
                resultDto = entityFindResponse.Results.FirstOrDefault();
            } else {
                logger.LogError($"Error while requesting Janrain info for UserUUId:{userUUID}. Error:{entityFindResponse?.Error}, ErrorMessage:{entityFindResponse?.ErrorDescription}");
            }

            logger.LogInformation("{class} - {method} - End", nameof(JanrainApiClient), nameof(JanrainApiClient.FindAsync));
            return resultDto;
        } catch (Exception ex) {
            logger.LogError($"Exception while calling Janrain entity Find API for UserUUId :{userUUID}, Error message:{ex.Message}, StackTrace:{ex.StackTrace}");
            throw;
        }
    }

    // 其他方法保持不变
}

为什么方案2更好?
  • IHttpClientFactory会自动管理HttpClient的Handler生命周期,避免传统单例HttpClient导致的Socket资源耗尽问题
  • 预先配置好的参数无需每次请求重复设置,减少代码冗余
  • HttpClient的请求方法(如GetAsync)是线程安全的,预配置后无需修改属性,完全避免并发冲突

内容的提问来源于stack exchange,提问作者Rakesh Kumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 17:47:34