C#-HttpClient请求超时排查记录
在一个 .NET Framework 4.7.2 控制台项目中,我用 HttpClient 向某 HTTPS 接口提交 POST 请求。相同接口和参数在 Postman、curl 中能很快收到响应,C# 却一直停在 SendAsync,最后超时。
这次排查得到两个不同层面的结论:关闭当前请求的 Expect: 100-continue 后,C# 能快速收到 HTTP 响应;要拿到正确的业务结果,还必须核对 Cookie 和表单格式。 文中的地址均为示例地址,不是真实接口。
问题代码
原始代码的核心部分如下,地址已替换:
var client = new HttpClient();
var request = new HttpRequestMessage(
HttpMethod.Post,
"https://example.com/api/check-status");
request.Content = new StringContent("applyData={\"apply\":[]}");
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
程序长时间停在 SendAsync。这里使用的是 await,没有 .Result 或 .Wait();单次请求也不足以支持“连接池耗尽”的判断,因此先对比两种客户端的实际行为。
curl 的“正常返回”是什么
不带登录 Cookie 请求该接口时,curl 很快收到 302 Found,响应包含指向站点入口页的 Location 和新设置的会话 Cookie。这表明当前请求没有得到预期的业务响应,很可能被会话校验拦截。
curl 默认不跟随重定向;HttpClientHandler 默认会跟随。为了看清接口的第一跳响应,诊断时关闭自动重定向:
var handler = new HttpClientHandler
{
AllowAutoRedirect = false
};
但关闭重定向后,C# 的首次请求仍会超时,说明自动跳转不是这次超时的直接原因。
真正影响超时的请求行为
使用同一套 .NET Framework 请求代码做对照测试,每次设置 20 秒超时,结果如下:
| 请求条件 | 结果 |
|---|---|
| 关闭自动重定向,强制 TLS 1.2 | 20 秒超时 |
| 在上述条件下再关闭代理 | 仍然超时 |
关闭 Expect: 100-continue |
约 245 毫秒收到 302 |
只对当前请求设置 ExpectContinue = false |
约 306 毫秒收到 302 |
同时,curl 分别使用 IPv4、IPv6 和不带 User-Agent 请求,都能快速收到响应。这些对照结果表明,本次超时与 .NET Framework 请求中的 Expect: 100-continue 交互有关;代理、地址族以及 User-Agent 不是已观察到的直接触发因素。
Expect: 100-continue 的作用是让客户端先发送请求头,等待服务器允许后再发送请求体。这个机制在正常实现中有等待和继续发送的处理,不能仅凭“服务器没有返回 100 Continue”就断定一定会无限等待。这里能确认的是:在当前客户端与目标站点的组合下,关闭它后超时消失。 没有服务端日志或完整链路抓包,无法进一步确定问题具体发生在源站、反向代理还是中间网关。
对这条请求,最小修改是在 SendAsync 前设置:
request.Headers.ExpectContinue = false;
这样只改变当前请求,不必调整整个进程的
收到响应后,还要修正业务请求
关闭 Expect 后收到的是 302,并非接口成功返回业务数据。原始代码还有两个明确的差异。
第一,代码没有携带登录 Cookie。新建的 HttpClient 不会继承 Postman 或浏览器中的登录会话。需要核对成功请求实际携带的 Cookie,并在 C# 中使用有效会话。未登录响应中新发的会话 Cookie,也不能直接当作已登录会话使用。
第二,new StringContent("applyData=...") 默认设置的是 Content-Type: text/plain; charset=utf-8。如果接口需要的是名为 applyData 的表单字段,应使用 FormUrlEncodedContent:
request.Content = new FormUrlEncodedContent(
new Dictionary<string, string>
{
["applyData"] = "{\"apply\":[]}"
});
这样会发送 application/x-www-form-urlencoded,并对字段值中的引号、花括号等字符进行表单编码。手工拼接在参数包含 &、+ 或中文时更容易出错。表单格式错误会影响业务处理,但不是本次已验证的超时原因:原来的 text/plain 请求在关闭 Expect 后也能迅速收到 302。
Referer、User-Agent 等请求头应以真正成功的请求为准,逐项比对。诊断中 curl 去掉 User-Agent 仍能快速得到 302,因此没有证据表明它导致了本次超时。TLS 1.2 和代理设置的单独调整也没有解决超时;关闭 Expect 后 .NET 能建立 HTTPS 连接并收到响应,因此当前现象也不符合典型的证书校验失败。
调整后的示例
下面的代码用于展示关键修改和诊断信息。示例假设接口采用表单提交;实际项目应以成功的 curl 请求或接口文档为准。Cookie 从环境变量读取,避免写入源码。
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
internal class Program
{
private static async Task Main(string[] args)
{
const string url = "https://example.com/api/check-status";
string cookie = Environment.GetEnvironmentVariable("API_COOKIE");
using (var handler = new HttpClientHandler
{
AllowAutoRedirect = false,
UseCookies = false
})
using (var client = new HttpClient(handler))
using (var request = new HttpRequestMessage(HttpMethod.Post, url))
{
request.Headers.ExpectContinue = false;
if (!string.IsNullOrWhiteSpace(cookie))
{
request.Headers.TryAddWithoutValidation("Cookie", cookie);
}
request.Content = new FormUrlEncodedContent(
new Dictionary<string, string>
{
["applyData"] = "{\"apply\":[]}"
});
using (var response = await client.SendAsync(request))
{
string body = await response.Content.ReadAsStringAsync();
Console.WriteLine(
$"HTTP {(int)response.StatusCode} {response.StatusCode}");
Console.WriteLine($"Location: {response.Headers.Location}");
Console.WriteLine(body);
response.EnsureSuccessStatusCode();
}
}
}
}
诊断时保留 AllowAutoRedirect = false,可以直接发现 Cookie 失效造成的 302。确认业务请求正常后,再决定是否需要自动跟随重定向。对于频繁调用接口的长期运行程序,还应复用 HttpClient,并继续及时释放每次请求和响应。
这次验证完成的是超时定位:关闭单次请求的 ExpectContinue 后,C# 已能快速收到 HTTP 状态码。由于没有用于测试的有效登录 Cookie,业务成功响应尚未验证。遇到类似情况,可以先加这一行,再观察首次响应的状态码和 Location;如果返回 302

浙公网安备 33010602011771号