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

