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

WebRequest超时异常:Timeout与Task.WhenAny均未生效问题排查

问题描述

我们通过PrepareWebRequest设置了100秒的超时时间,但部分HTTP请求未按预期触发超时错误,反而在40分钟后抛出System.IO.IOException:

Unable to read data from the transport connection: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond

该问题为偶发情况。

在GetResponseStreamThrow方法中,我们尝试了两种调用方式,但WebRequest.Timeout和基于Task.Delay的自定义超时均未生效:

WebResponse ws = await TaskWithTimeout(request.GetResponseAsync(), request.Timeout).ConfigureAwait(false);
WebResponse ws = TaskWithTimeout(Task.Run(request.GetResponse), request.Timeout).Result;

通过Fiddler可观察到请求已完成握手。请问这是否属于.NET Bug?同时,在该场景下有什么更优的客户端超时实现方案?

附相关代码:

private Stream PostData(WebRequest request, byte[] rawByteData)
{
    Stream req = request.GetRequestStream();
    req.Write(rawByteData, 0, rawByteData.Length);

    return req;
}

public WebRequestResult RequestPostDataWebRequestResult(string url, string postData, IEnumerable<string> headers = null, string contentType = null, int timeout = 300)
{
    WebRequestResult webRequestResult = new WebRequestResult();
    try
    {
        byte[] rawByteData;
        WebRequest request = PrepareWebRequest(url, postData, headers, out rawByteData, contentType, timeout:timeout);

        Stream req = PostData(request, rawByteData);

        var response = GetResponseStreamThrow(request);

        req.Close();
        webRequestResult.Message = response.Item1;
        webRequestResult.StatusCode = response.Item2;
        webRequestResult.Status = true;
        return webRequestResult;
    }
    catch (Exception e)
    {
        webRequestResult.Message = e.ToString();
        webRequestResult.Status = false;
        if (e is WebException webException)
        {
            webRequestResult.WebExceptionStatus = webException.Status;
            if (webException.Response is HttpWebResponse response)
            {
                webRequestResult.StatusCode = response.StatusCode;
            }
        }
        return webRequestResult;
    }
}
private async Task<WebResponse> TaskWithTimeout(Task<WebResponse> task, int duration)
{
    if (duration == 0)
        duration = 100 * 1000;
    var retTask = await Task.WhenAny(task, Task.Delay(duration)).ConfigureAwait(false);

    if (retTask is Task<WebResponse>) return task.Result;
    return null;
}

问题分析与解决方案

是否属于.NET Bug?

不是。WebRequest.Timeout仅控制请求阶段的超时(比如建立连接、获取响应头的时间),当连接握手完成后,如果服务器迟迟不返回响应体,WebRequest.Timeout不会触发——因为它不覆盖响应体读取阶段的超时。你遇到的40分钟后抛出的IOException,其实是底层TCP连接的默认空闲超时(系统级设置,通常为几十分钟),和WebRequest的超时机制无关。

另外你的自定义超时逻辑存在缺陷:

  • 使用Task.WhenAny后,即使超时触发,原request.GetResponseAsync()任务仍在后台运行,未被取消,既浪费资源,还可能引发后续异常。
  • 调用task.Result会阻塞线程,若原任务抛出异常,会被包装成AggregateException,增加异常处理复杂度。

更优的客户端超时实现方案

1. 用CancellationToken实现可取消的超时

替换自定义超时逻辑,通过CancellationTokenSource绑定超时,同时取消原请求任务:

private async Task<WebResponse> TaskWithTimeout(WebRequest request, int duration)
{
    if (duration == 0)
        duration = 100 * 1000;

    using var cts = new CancellationTokenSource(duration);
    // 绑定WebRequest的Abort方法到取消令牌
    cts.Token.Register(() => request.Abort());

    try
    {
        return await request.GetResponseAsync().ConfigureAwait(false);
    }
    catch (WebException ex) when (cts.Token.IsCancellationRequested)
    {
        // 明确抛出超时异常
        throw new TimeoutException("请求超时", ex);
    }
}

2. 区分不同阶段的超时

如果需要更精细的控制,可分别设置:

  • 连接/请求头超时:保留WebRequest.Timeout设置(对应原逻辑的100秒)。
  • 响应体读取超时:获取响应流后,设置Stream.ReadTimeout控制读取响应体的超时:
using var response = await TaskWithTimeout(request, request.Timeout).ConfigureAwait(false);
using var responseStream = response.GetResponseStream();
responseStream.ReadTimeout = 30 * 1000; // 设置30秒读取超时
// 读取响应流的逻辑...

3. 替换为HttpClient(推荐)

WebRequest是较旧的API,HttpClient提供了更简洁、统一的超时控制,且支持现代异步编程:

  • HttpClient.Timeout控制整个请求生命周期的超时(包括连接、发送请求、读取响应)。
  • 支持通过CancellationTokenSource实现灵活的超时逻辑。

示例代码:

public async Task<WebRequestResult> RequestPostDataHttpClient(string url, string postData, IEnumerable<string> headers = null, string contentType = null, int timeoutSeconds = 100)
{
    var webRequestResult = new WebRequestResult();
    using var httpClient = new HttpClient();
    httpClient.Timeout = TimeSpan.FromSeconds(timeoutSeconds);

    var content = new StringContent(postData);
    if (!string.IsNullOrEmpty(contentType))
    {
        content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType);
    }
    if (headers != null)
    {
        foreach (var header in headers)
        {
            var split = header.Split(':');
            if (split.Length == 2)
            {
                httpClient.DefaultRequestHeaders.TryAddWithoutValidation(split[0].Trim(), split[1].Trim());
            }
        }
    }

    try
    {
        using var response = await httpClient.PostAsync(url, content).ConfigureAwait(false);
        response.EnsureSuccessStatusCode();
        var responseContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
        
        webRequestResult.Message = responseContent;
        webRequestResult.StatusCode = response.StatusCode;
        webRequestResult.Status = true;
    }
    catch (TaskCanceledException ex) when (!ex.CancellationToken.IsCancellationRequested)
    {
        // 捕获超时引发的取消异常
        webRequestResult.Message = "请求超时";
        webRequestResult.Status = false;
    }
    catch (HttpRequestException ex)
    {
        webRequestResult.Message = ex.ToString();
        webRequestResult.Status = false;
        if (ex.StatusCode.HasValue)
        {
            webRequestResult.StatusCode = ex.StatusCode.Value;
        }
    }
    catch (Exception ex)
    {
        webRequestResult.Message = ex.ToString();
        webRequestResult.Status = false;
    }
    return webRequestResult;
}

4. 修复原代码的资源泄漏问题

原代码中PostData方法返回Stream后,仅在成功路径调用req.Close(),异常路径会导致流未关闭,建议用using语句自动释放资源:

private void PostData(WebRequest request, byte[] rawByteData)
{
    using var req = request.GetRequestStream();
    req.Write(rawByteData, 0, rawByteData.Length);
}

内容的提问来源于stack exchange,提问作者enes.tirnakci

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 14:40:07