同一接口 RestClient 正常、HttpClient 返回 404:一次真实请求差异排查
环境:.NET Framework 4.7.2、RestSharp 110.2.0。本文的域名、路径、业务编号和 Cookie 均使用示例值。
问题现象
调用同一个 HTTPS 接口时,问题分成了两个阶段:
-
Postman 和 curl 能很快收到响应,最初的 C#
HttpClient请求却停在SendAsync,随后超时。 -
设置
初看两个方法的 URL、POST 参数和请求头几乎相同,因此重点不是比较写出来的代码,而是比较最终发到服务器的请求。
当前代码的关键部分
dome1 使用 RestSharp,代码结构如下。实际业务值已替换:
var options = new RestClientOptions("https://example.com")
{
MaxTimeout = -1,
UserAgent = "Mozilla/5.0 ..."
};
var client = new RestClient(options);
var request = new RestRequest("/api/check-status", Method.Post);
request.AddHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
request.AddHeader("Cookie", cookie);
var body = "bookingno=TEST001&location=PORT"
+ "&applyData=%7B%22apply%22%3A%5B%5D%7D"
+ "&random=0.123";
request.AddParameter(
"application/x-www-form-urlencoded",
body,
ParameterType.RequestBody);
RestResponse response = await client.ExecuteAsync(request);
dome2 最初的关键代码如下。它添加了相同的 Cookie 字符串,但创建 HttpClient 时没有配置处理器:
var client = new HttpClient();
var request = new HttpRequestMessage(
HttpMethod.Post,
"https://example.com/api/check-status");
request.Headers.Add("Cookie", cookie);
request.Content = new StringContent(
body,
Encoding.UTF8,
"application/x-www-form-urlencoded");
var response = await client.SendAsync(request);
两个方法中还设置了相同的 Accept、Referer、Origin、User-Agent 等请求头。它们都有一处拼写为 Aceept 的额外请求头;这个拼写错误应单独清理,但它在两边都存在,无法解释本次结果差异。
排查过程
1. 先解决超时
用 .NET Framework 请求对同一接口做对照:关闭自动重定向、强制 TLS 1.2,仍在 20 秒后超时;再关闭代理,也没有改变结果。关闭 Expect: 100-continue 后,约 245 毫秒就收到 HTTP 响应;仅对当前请求设置 request.Headers.ExpectContinue = false,约 306 毫秒收到响应。
Expect: 100-continue 允许客户端先发请求头,等待服务器允许后再发送请求体。对这个很小的表单请求没有保留该机制的必要。对照测试说明本次超时与这段交互有关;没有服务端日志或完整抓包,无法确定具体是源站还是中间网关处理不兼容。关闭它解决的是等待问题,不代表接口已经返回了正确业务数据。
项目后来在 Main 中加入了:
ServicePointManager.Expect100Continue = false;
修正后的 dome2 还对单条请求明确设置了:
request.Headers.ExpectContinue = false;
2. 对比真正发出的请求
为排除服务端业务逻辑干扰,我使用项目中的 RestSharp 110.2.0 和 .NET Framework HttpClient,向本地接收端发送同样的示例 URL、POST Body 和假 Cookie。接收端观察到:
| 项目 | dome1 对应的 RestSharp | 原版 dome2 对应的 HttpClient |
|---|---|---|
| 请求行 | POST /api/check-status HTTP/1.1 |
相同 |
| Body | 同一段已编码的表单文本 | 相同 |
| Content-Type | application/x-www-form-urlencoded; charset=UTF-8 |
同类型,仅 utf-8 大小写不同 |
Expect |
未发送 | 关闭全局设置后未发送 |
| Cookie | 已发送 | 未发送 |
这就是第二阶段的关键差异:代码里调用了 request.Headers.Add("Cookie", cookie),不等于 Cookie 一定出现在网络请求中。 在本项目所用的 .NET Framework 实现里,默认 HttpClientHandler 的 UseCookies = true 会由其 CookieContainer 管理 Cookie,手动添加的 Cookie 头没有按预期发出。RestSharp 的这次请求则确实发出了手动添加的 Cookie。
两边的 Body 字节内容在本地测试中一致。当前 body 已经是百分号编码后的完整表单文本,用 StringContent 发送并不会再编码一次。不能把这段已经编码的 applyData=%7B... 直接作为 FormUrlEncodedContent 的字段值,否则可能把 % 再编码为 %25,造成双重编码。
3. 解释为什么最终看到 404
此前对目标站点的无 Cookie 请求测试得到的第一跳响应是 302 Found,Location 指向站点入口页;继续跟随该跳转后,入口页返回 404。
RestSharp 110.2.0 和 HttpClientHandler 都默认跟随重定向。结合本地抓到的 Cookie 差异,本次现象可以串起来:
dome1:POST + 有效 Cookie → 接口返回业务数据
dome2:POST + Cookie 实际缺失 → 302 到入口页
→ 客户端自动跟随,后续请求改为 GET
→ 入口页返回 404
这里的 404 很可能属于重定向后的地址,不是最初的 POST 接口。要在现场确认,可以临时设 AllowAutoRedirect = false,查看第一跳的状态码及 Location;也可以查看自动跳转后 response.RequestMessage.RequestUri 指向哪里。
原因分析
这次不是一个故障反复出现,而是两个先后暴露的问题:
-
最初的超时:在当前客户端与目标站点的组合下,
Expect: 100-continue相关交互导致请求迟迟拿不到响应;关闭后恢复快速响应。 -
之后的 404:
dome2默认启用HttpClientHandler.UseCookies,手动写入的 Cookie 没有发出;无 Cookie 请求被重定向,自动跟随后看到 404。
URL、HTTP Method 和 Body 在原始请求中并没有发现差异。两边 Content-Type 的字符集大小写不同,但媒体类型与字符集含义相同,不是这次 404 的原因。dome1 的 MaxTimeout = -1 与 HttpClient 默认超时时间也不能解释一个已经返回的 404。
解决方案
如果继续使用手动拼接的 Cookie 请求头,就明确关闭处理器的自动 Cookie 管理,并对齐 RestSharp 实际发出的 Body 和 Content-Type:
var handler = new HttpClientHandler
{
UseCookies = false,
AllowAutoRedirect = true // 与当前 RestSharp 默认行为一致
};
using (var client = new HttpClient(handler))
using (var request = new HttpRequestMessage(HttpMethod.Post, url))
{
request.Headers.ExpectContinue = false;
// 其余 Accept、Referer、Origin、User-Agent 等头沿用 dome1 的值。
request.Headers.TryAddWithoutValidation("Cookie", cookie);
// body 是与 dome1 相同的、已经编码好的完整表单文本。
request.Content = new StringContent(body, Encoding.UTF8);
request.Content.Headers.ContentType =
System.Net.Http.Headers.MediaTypeHeaderValue.Parse(
"application/x-www-form-urlencoded;charset=UTF-8");
using (var response = await client.SendAsync(request))
{
Console.WriteLine($"HTTP {(int)response.StatusCode}");
Console.WriteLine($"最终地址:{response.RequestMessage.RequestUri}");
string result = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();
return result;
}
}
当前项目的 dome2 已采用上述关键设置,并保留原有的 URL、Body、Cookie 和其他请求头。本地接收端复测时,修正后的 HttpClient 与 RestSharp 发出了相同的 POST 路径、Cookie、Body 和 Content-Type;项目也已编译通过。本地请求一致性已经验证,线上业务返回值仍取决于当前 Cookie 是否有效。
如果更希望由 CookieContainer 管理 Cookie,也可以保留 UseCookies = true,把 Cookie 正确加入该容器;此时不要再依赖手动的 Cookie 请求头。两种方式选一种即可。
以后遇到类似问题怎么查
-
先看第一跳状态码。 暂时关闭自动重定向,记录
StatusCode和Location;不要把跳转后的 404 当成原接口的 404。 -
对比实际发出的请求。 至少核对最终 URL、Method、Cookie、
Content-Type和 Body 字节;代码中“设置过”某个头,不代表它已经发送。 -
确认编码层次。 区分原始字段值与已经编码好的完整表单文本,避免漏编码或双重编码。
-
遇到超时再检查连接交互。 在相同条件下单独切换
ExpectContinue,然后排查 TLS、代理、DNS 与地址族;用结果定位,不靠猜测。 -
最后验证业务响应。 确认使用有效会话,并记录原始状态码、最终地址和响应内容,区分“收到了 HTTP 响应”和“拿到了业务数据”。
这次最有用的教训是:比较 dome1 和 dome2 时,真正决定结果的是网络上出现的请求。表面相同的 Cookie

浙公网安备 33010602011771号