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

.NET Framework 4.8下WinHttpException(12002)请求超时问题求助

大量分页API请求触发WinHttpException超时错误(Error 12002)

问题背景

应用需向外部Web API发起超2000次分页请求,但随机次数后(通常约1000次,最少200次)触发以下错误:

System.Net.Http.WinHttpException: Error 12002 calling WINHTTP_CALLBACK_STATUS_REQUEST_ERROR, 'Timeout dell'operazione'

已尝试方案:

  • 每20次请求销毁并重新初始化HttpClient
  • 将HttpClient超时时间设置为5小时
    问题依旧存在,运行环境为.NET Framework 4.8。

相关代码

HttpClient初始化方法

public static void InitializeClient()
{
    var handler = new WinHttpHandler(); //used to allow body with GET in .NET Framework
    myHttpClient = new HttpClient(handler);
    myHttpClient.Timeout = TimeSpan.FromHours(5);
    myHttpClient.BaseAddress = new Uri(Config.APIUrl);
    myHttpClient.DefaultRequestHeaders.Accept.Clear();
    myHttpClient.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json") { CharSet = Encoding.UTF8.WebName });
    myHttpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Token", Config.AuthToken);
}

测试触发方法

public async void GetCustomers_ShouldWork()
{
    HTTPClient.InitializeClient();

    var ex = await Record.ExceptionAsync(() => HTTPClient.GetCustomers());

    Assert.Null(ex);
}

核心请求方法

public static async Task<List<ModelCustomers>> GetCustomers()
{          
    try
    {
        int contPageRequestSameClient = 0;
        int maxPageRequestSameClient = 20;

        List<ModelCustomers> modelCustomers = new List<ModelCustomers>();

        DateTime entityDateLastImportSuccess = DBClient.ReadEntityDateLastImportSuccess(Config.Entities.Customer.ToString());

        List<string> requestFields = new List<string> /*{ "number", "id" }*/{ };

        int requestPage = 1;
        bool hasNextPage = true;

        while (hasNextPage)
        {
            SyncLogger.RegisterLog($"Request page: {requestPage}");

            var getRequestBody = new ModelCustomersRequestBody
            {
                page = requestPage,
                fields = requestFields,
                q = new ModelCustomersRequestBodyFilter { updated_at_gt = entityDateLastImportSuccess.ToString() }
            };

            string jsonGetRequestBody = JsonConvert.SerializeObject(getRequestBody);

            HttpRequestMessage request = new HttpRequestMessage
            {
                Method = HttpMethod.Get,
                RequestUri = new Uri(myHttpClient.BaseAddress + "customers"),
                Content = new StringContent(jsonGetRequestBody, Encoding.UTF8, "application/json")
            };

            HttpResponseMessage response = await myHttpClient.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                try
                {
                    ModelCustomers responseData = await response.Content.ReadAsAsync<ModelCustomers>();

                    modelCustomers.Add(responseData);

                    int? nextPage = responseData.info.pagination.nextPage;

                    hasNextPage = nextPage.HasValue;

                    if (hasNextPage)
                    {
                        requestPage++;
                    }
                }
                catch (Exception)
                {
                    string responseString;
                    responseString = await response.Content.ReadAsStringAsync();
                    SyncLogger.RegisterLog($"Json in error: { responseString } ");
                    throw;
                }
            }
            else
            {
                SyncLogger.RegisterLog($"Response: {response.ReasonPhrase + response.ToString()}");

                throw new Exception("HTTP response not IsSuccessStatusCode");
            }

            contPageRequestSameClient++;

            if (contPageRequestSameClient == maxPageRequestSameClient)
            {
                //reset client
                myHttpClient.Dispose();
                InitializeClient();
                contPageRequestSameClient = 0;
                SyncLogger.RegisterLog($"HTTP Client reset");
            }
        }
        return modelCustomers;
    }
    catch (Exception ex)
    {
        //SyncLogger.RegisterLog(ex.ToString());
        throw;
    }
}

错误堆栈

System.Net.Http.HttpRequestException: An error occurred while sending the request. ---> System.Net.Http.WinHttpException: Error 12002 calling WINHTTP_CALLBACK_STATUS_REQUEST_ERROR, 'Timeout dell'operazione'.
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Threading.Tasks.RendezvousAwaitable`1.GetResult()
   at System.Net.Http.WinHttpHandler.<StartRequestAsync>d__122.MoveNext()
   --- End of inner exception stack trace ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()

原因分析

  1. WinHttpHandler连接池限制:WinHttpHandler依赖Windows原生WinHTTP API,默认连接池存在并发数、空闲连接存活时间限制,大量连续请求会耗尽连接池,后续请求等待连接时触发超时。
  2. GET请求带Body的非常规用法:虽然WinHttpHandler允许GET请求带Body,但不符合HTTP规范,部分服务器或代理对这类请求处理异常,导致超时。
  3. 资源未正确释放:HttpRequestMessage、HttpResponseMessage及内容未显式释放,长期运行会引发句柄或内存泄漏,间接导致超时。
  4. 服务器端限流:外部API可能对高频请求有限流策略,触发后服务器延迟响应或断开连接,表现为客户端超时。

解决办法

1. 调整WinHttpHandler连接池配置

初始化时设置连接池参数,匹配API允许的并发能力:

var handler = new WinHttpHandler();
handler.MaxConnectionsPerServer = 10; // 根据API限制调整
handler.MaxAutomaticRedirections = 5;
handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;

2. 替换GET带Body的请求方式

遵循HTTP规范,将参数放到QueryString中:

// 构建查询参数
var queryParams = new Dictionary<string, string>
{
    {"page", requestPage.ToString()},
    {"updated_at_gt", entityDateLastImportSuccess.ToString("yyyy-MM-ddTHH:mm:ss")}
};
if (requestFields.Any())
{
    queryParams.Add("fields", string.Join(",", requestFields));
}

// 构造带QueryString的请求URI
var requestUri = new UriBuilder(myHttpClient.BaseAddress + "customers")
{
    Query = string.Join("&", queryParams.Select(kv => $"{Uri.EscapeDataString(kv.Key)}={Uri.EscapeDataString(kv.Value)}"))
}.Uri;

// 创建不带Body的GET请求
using (var request = new HttpRequestMessage(HttpMethod.Get, requestUri))
{
    using (var response = await myHttpClient.SendAsync(request))
    {
        // 响应处理逻辑
    }
}

3. 严格释放HTTP资源

使用using语句确保每次请求的资源被正确释放:

// 循环内的请求和响应都用using包裹
using (var request = new HttpRequestMessage(/*参数*/))
{
    using (var response = await myHttpClient.SendAsync(request))
    {
        // 处理响应
    }
}

4. 添加重试与请求延迟

针对限流或临时超时,添加指数退避重试和请求间隔:

// 安装Polly NuGet包后使用重试策略
var retryPolicy = Policy
    .Handle<WinHttpException>(ex => ex.Message.Contains("12002"))
    .Or<HttpRequestException>()
    .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));

// 发送请求时应用重试
using (var response = await retryPolicy.ExecuteAsync(() => myHttpClient.SendAsync(request)))
{
    // 处理响应
}

// 请求间添加短暂延迟(根据API速率限制调整)
await Task.Delay(100);

5. 优化HttpClient生命周期

避免频繁Dispose HttpClient(会破坏连接池),建议使用静态单例或IHttpClientFactory(.NET Framework 4.8可通过NuGet安装Microsoft.Extensions.Http使用)。若必须重置,Dispose后等待1-2秒再初始化新客户端。


内容的提问来源于stack exchange,提问作者Pietro Ariano

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 00:44:58