使用 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.ErrorCodeE.ErrorMessageCSHttp.Response.ResponseTextCSHttp.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 实例后恢复,也可能说明其中一个节点状态异常。应在响应中增加或检查节点标识,并对比各节点的日志、配置和许可状态。
建议的排查顺序
- 捕获
EIdHTTPProtocolException,保存 402 的响应正文和响应头。 - 对比成功与失败计算机生成的最终 URL、账号和 DNS 解析结果。
- 检查是否存在代理、网关、安全软件或不同的网络出口。
- 根据错误发生时间,在 ADS/Adsweb 日志中查找对应请求。
- 查看当时的活动会话、连接池状态,以及许可或并发限制。
- 使用
Request.Connection := 'close'做一次对照测试。 - 完整编码
statement参数,不再手工拼接部分%20。 - 如果存在多个 Adsweb/ADS 节点,分别直接访问每个节点测试。
客户端代码需要改进异常记录、对象生命周期和 URL 编码。对于 402 本身,应从服务器返回的响应正文、日志、会话和许可状态入手调查。反复重启 ADS 只能暂时清理现场,无法代替对根因的确认。
备注:内容仅供参考。
