PAYATHON 2026

使用 TidHTTP 访问 Adsweb 时偶发 HTTP 402 错误

支付阿杰

结论

这段 TIdHTTP 调用没有明显错误会直接导致 HTTP 402。TIdHTTP 只是把服务器返回的状态码以异常形式报告出来,实际的 402 响应来自 Adsweb、ADS,或者它们前面的代理服务器。

同一程序在不同计算机上结果不同,通常要从几方面排查:服务器端的会话或许可资源、客户端使用的代理、实际请求的 URL 和账号,以及请求参数是否完整编码。重启 ADS 11 后能暂时恢复,更像是服务器端连接、会话或许可资源被占用,而不是同一条 SQL 语句换一台计算机就失效了。

不过,只看 E.Message 无法确定 Adsweb 如何定义 402。还需要检查响应正文和 ADS/Adsweb 日志,才能判断它表示许可限制、会话异常,还是其他应用级错误。

获取完整的 HTTP 错误信息

Indy 收到 4xx 或 5xx 响应时会抛出 EIdHTTPProtocolException。当前代码只保存 E.Message,可能遗漏服务器返回的详细说明。

可以单独捕获该异常:

uses
  IdHTTP, IdException, SysUtils;

var
  CSHttp: TIdHTTP;
  MyResult: string;
  MyError: string;
begin
  CSHttp := TIdHTTP.Create(nil);
  try
    try
      MyResult := CSHttp.Get(MyURL);
    except
      on E: EIdHTTPProtocolException do
      begin
        MyError :=
          'HTTP status: ' + IntToStr(E.ErrorCode) + sLineBreak +
          'Message: ' + E.Message + sLineBreak +
          'Response body: ' + E.ErrorMessage;
      end;
      on E: Exception do
        MyError := E.ClassName + ': ' + E.Message;
    end;
  finally
    CSHttp.Free;
  end;
end;

建议记录:

  • E.ErrorCode
  • E.ErrorMessage
  • CSHttp.Response.ResponseText
  • CSHttp.Response.Server
  • 最终请求 URL
  • 请求使用的账号
  • 错误发生时间和客户端 IP

不要把数据库密码写入日志。

修正对象的创建和释放结构

原代码进入 try 后才创建对象。如果 TIdHTTP.Create 本身抛出异常,finally 可能会释放一个尚未初始化的局部变量。更安全的写法是先创建对象,再进入 try...finally:

CSHttp := TIdHTTP.Create(nil);
try
  { 使用 CSHttp }
finally
  CSHttp.Free;
end;

也可以先明确初始化:

CSHttp := nil;
try
  CSHttp := TIdHTTP.Create(nil);
  { 使用 CSHttp }
finally
  CSHttp.Free;
end;

这不是 HTTP 402 的主要原因,但代码结构应当修正。

正确编码查询参数

当前代码只是把 SQL 中的空格手工替换成 %20,没有统一编码 Myphone,也没有处理 SQL 中的引号、+、&、#、% 等字符。如果不同计算机处理的数据内容不同,最终发送的 URL 也可能不同。

应先构造完整的 SQL,再编码整个 statement 参数。具体使用哪个编码函数取决于 Indy 版本,例如:

uses
  IdHTTP, IdURI, SysUtils;

var
  CSHttp: TIdHTTP;
  BaseURL: string;
  SQLText: string;
  MyURL: string;
  MyResult: string;
begin
  CSHttp := TIdHTTP.Create(nil);
  try
    BaseURL := MasterTbl.FieldByName('DatabaseUrl').AsString;

    SQLText :=
      'select name,accountnum from customer ' +
      'where accountnum=''' +
      StringReplace(Myphone, '''', '''''', [rfReplaceAll]) +
      '''';

    MyURL :=
      BaseURL +
      '__query?statement=' + TIdURI.ParamsEncode(SQLText) +
      '&$format=json';

    CSHttp.Request.CacheControl := 'no-cache';
    CSHttp.Request.BasicAuthentication := True;
    CSHttp.Request.Username :=
      MasterTbl.FieldByName('DBlogin').AsString;
    CSHttp.Request.Password :=
      MasterTbl.FieldByName('DBPassword').AsString;
    CSHttp.Request.UserAgent :=
      'Mozilla/3.0 (compatible; Indy Library)';

    MyResult := CSHttp.Get(MyURL);
  finally
    CSHttp.Free;
  end;
end;

如果当前 Indy 版本没有 TIdURI.ParamsEncode,应改用该版本提供的查询参数编码函数。不要直接编码整个 URL,否则 :、/、?、& 等 URL 结构字符也可能被错误处理。

将单引号双写只能防止 SQL 字符串被提前结束。更合适的做法是使用 Adsweb 支持的参数化查询。是否支持以及具体语法,应以当前 Adsweb 接口文档为准。

为什么不同计算机会得到不同结果

建议按以下几方面逐项核对。

1. 实际请求并不完全相同

记录两台计算机最终生成的 URL,同时隐藏账号、电话号码等敏感数据。重点检查:

  • DatabaseUrl 是否完全一致
  • 是否多了或少了 /
  • Myphone 是否含有空格、+、& 或非 ASCII 字符
  • 登录账号和密码是否一致
  • DNS 是否将同一主机名解析到不同服务器

2. 使用了不同的代理或网关

某台计算机可能通过系统代理、透明代理、安全软件或企业网关访问 Adsweb。402 可能是这些中间组件返回的,并非来自 ADS。

可以比较成功和失败响应中的 Server、Via 等响应头,并在环境允许时直接访问 Adsweb 地址进行验证。

3. ADS/Adsweb 的会话或许可资源没有及时释放

重启 ADS 后能够暂时恢复,因此应优先检查这一方向。可能的情况包括:

  • 活跃连接或会话达到限制
  • 客户端异常断开后,服务器会话没有及时清理
  • Adsweb 连接池没有正常回收连接
  • 当前许可或并发策略拒绝新请求

HTTP 标准将 402 命名为 Payment Required,但应用服务器也可以用它表示自定义的许可或服务状态。不能仅凭状态文本判断需要“付款”,应以响应正文和服务器日志中的错误说明为准。

可以临时关闭连接复用,进行对照测试:

CSHttp.Request.Connection := 'close';

如果这样能够稳定避免问题,应继续检查 Adsweb 的 Keep-Alive、连接池和会话回收配置。这个设置适合用来诊断,不一定是最终解决方案。

4. 服务器节点状态不同

如果 Adsweb 后面有负载均衡,不同计算机可能被分配到不同节点。重启某个 ADS 实例后恢复,也可能说明其中一个节点状态异常。应在响应中增加或检查节点标识,并对比各节点的日志、配置和许可状态。

建议的排查顺序

  1. 捕获 EIdHTTPProtocolException,保存 402 的响应正文和响应头。
  2. 对比成功与失败计算机生成的最终 URL、账号和 DNS 解析结果。
  3. 检查是否存在代理、网关、安全软件或不同的网络出口。
  4. 根据错误发生时间,在 ADS/Adsweb 日志中查找对应请求。
  5. 查看当时的活动会话、连接池状态,以及许可或并发限制。
  6. 使用 Request.Connection := 'close' 做一次对照测试。
  7. 完整编码 statement 参数,不再手工拼接部分 %20。
  8. 如果存在多个 Adsweb/ADS 节点,分别直接访问每个节点测试。

客户端代码需要改进异常记录、对象生命周期和 URL 编码。对于 402 本身,应从服务器返回的响应正文、日志、会话和许可状态入手调查。反复重启 ADS 只能暂时清理现场,无法代替对根因的确认。

备注:内容仅供参考。