同一接口 RestClient 正常、HttpClient 返回 404:一次真实请求差异排查

环境:.NET Framework 4.7.2、RestSharp 110.2.0。本文的域名、路径、业务编号和 Cookie 均使用示例值。

问题现象

调用同一个 HTTPS 接口时,问题分成了两个阶段:

  1. Postman 和 curl 能很快收到响应,最初的 C# HttpClient 请求却停在 SendAsync,随后超时。

  2. 设置 ServicePointManager.Expect100Continue = false 后,C# 不再超时;但项目中的 dome1(RestClient)能拿到业务数据,dome2(HttpClient)最终得到 404。

初看两个方法的 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 指向哪里。

原因分析

这次不是一个故障反复出现,而是两个先后暴露的问题:

  1. 最初的超时:在当前客户端与目标站点的组合下,Expect: 100-continue 相关交互导致请求迟迟拿不到响应;关闭后恢复快速响应。

  2. 之后的 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 请求头。两种方式选一种即可。

以后遇到类似问题怎么查

  1. 先看第一跳状态码。 暂时关闭自动重定向,记录 StatusCode 和 Location;不要把跳转后的 404 当成原接口的 404。

  2. 对比实际发出的请求。 至少核对最终 URL、Method、Cookie、Content-Type 和 Body 字节;代码中“设置过”某个头,不代表它已经发送。

  3. 确认编码层次。 区分原始字段值与已经编码好的完整表单文本,避免漏编码或双重编码。

  4. 遇到超时再检查连接交互。 在相同条件下单独切换 ExpectContinue,然后排查 TLS、代理、DNS 与地址族;用结果定位,不靠猜测。

  5. 最后验证业务响应。 确认使用有效会话,并记录原始状态码、最终地址和响应内容,区分“收到了 HTTP 响应”和“拿到了业务数据”。

这次最有用的教训是:比较 dome1 和 dome2 时,真正决定结果的是网络上出现的请求。表面相同的 Cookie 添加代码,在不同客户端的默认处理器配置下,可能变成完全不同的实际请求。

posted @ 2026-09-23 23:00  曲琦  阅读(7)  评论(0)    收藏  举报