如何在 PHP 中处理 NuSOAP 的 300 秒超时?
结论
NuSOAP 可以通过构造函数中的连接超时和响应超时参数,把等待响应的时间设为 300 秒。发生超时后,通常不会得到正常的 SOAP 业务结果,错误一般记录在 getError() 中,也通常不属于 SOAP Fault。
需要注意,NuSOAP 的 response_timeout 主要限制网络读取时间,并不等于从调用开始计时,满 300 秒就一定会立即中断。DNS 查询、连接建立、WSDL 加载以及底层网络实现都可能影响实际耗时。判断是否超时时,需要同时检查:
doPayment()是否返回有效结果;$client->getError()中是否有超时类错误;- 实际耗时是否接近或超过 300 秒;
- 是否收到了 SOAP Fault。
支付请求超时不代表支付失败。服务端可能已经完成扣款,只是响应没有成功返回。遇到这种情况,应使用同一个交易号调用 getStatus(),不要再次提交 doPayment()。
配置 NuSOAP 超时
常见 NuSOAP 版本的构造函数末尾有两个超时参数:
new nusoap_client(
$endpoint,
$wsdl,
$proxyHost,
$proxyPort,
$proxyUsername,
$proxyPassword,
$timeout,
$responseTimeout
);
其中:
$timeout:建立连接的超时时间;$responseTimeout:等待和读取响应的超时时间。
下面的配置将连接超时设为 10 秒,响应超时设为 300 秒:
<?php
$wsdlUrl = 'http://webserviceurl?wsdl';
$client = new nusoap_client(
$wsdlUrl,
'wsdl',
false,
false,
false,
false,
10,
300
);
$error = $client->getError();
if ($error) {
throw new RuntimeException('NuSOAP constructor error: ' . $error);
}
$proxy = $client->getProxy();
不同 NuSOAP 分支或旧版本的构造函数签名可能略有不同。如果当前版本不接受这两个参数,需要检查所用版本中的 nusoap_client 构造函数。也可以在创建客户端后设置:
$client->timeout = 10;
$client->response_timeout = 300;
建议优先通过构造函数传参,因为加载 WSDL 时也可能发生网络连接。
判断 doPayment() 是否超时
NuSOAP 通常没有统一且稳定的 timeout 错误码。不同版本、PHP 网络流或 cURL 后端返回的错误文字可能不同,例如包含 timeout、timed out 或类似内容。因此,不能只匹配某一句固定的错误信息。
可以结合错误内容与调用耗时判断:
<?php
function looksLikeTimeout($error, $elapsedSeconds, $limitSeconds)
{
if (!$error) {
return false;
}
$hasTimeoutMessage = preg_match(
'/timeout|timed\s*out|operation\s+timed\s+out/i',
$error
) === 1;
// 留出少量计时误差,避免因浮点时间或底层实现差异漏判。
$reachedTimeLimit = $elapsedSeconds >= ($limitSeconds - 1);
return $hasTimeoutMessage || $reachedTimeLimit;
}
调用支付接口前,应生成并持久化一个唯一交易号:
<?php
$transactionId = 'PAY-' . bin2hex(random_bytes(12));
$paymentParams = [
'transactionId' => $transactionId,
'someparams' => 'somevalues',
];
// 实际项目中应先将交易号和“处理中”状态保存到数据库。
$startedAt = microtime(true);
$payTrans = $proxy->doPayment($paymentParams);
$elapsed = microtime(true) - $startedAt;
$error = $client->getError();
$timedOut = looksLikeTimeout($error, $elapsed, 300);
不要只根据 $payTrans === false 判断超时。连接失败、HTTP 错误和 XML 解析失败等情况也可能返回 false。
SOAP Fault 需要单独处理:
if ($client->fault) {
// 这是服务端返回的 SOAP Fault,并不等同于网络超时。
// 根据网关文档检查 faultcode、faultstring 等字段。
}
超时后调用 getStatus() 三次
确认是超时类故障后,可以使用原交易号连续查询三次:
<?php
if ($timedOut) {
$statusResults = [];
for ($attempt = 1; $attempt <= 3; $attempt++) {
$status = $proxy->getStatus([
'transactionId' => $transactionId,
]);
$statusError = $client->getError();
$statusResults[] = [
'attempt' => $attempt,
'result' => $status,
'error' => $statusError,
'fault' => $client->fault,
];
}
// 根据支付网关定义的状态字段判断:
// 1. 支付成功;
// 2. 明确失败;
// 3. 仍在处理中或状态未知。
}
如果网关文档允许在查到明确的成功状态后停止,可以提前退出循环:
for ($attempt = 1; $attempt <= 3; $attempt++) {
$status = $proxy->getStatus([
'transactionId' => $transactionId,
]);
$statusError = $client->getError();
if (!$statusError && !$client->fault && isSuccessfulStatus($status)) {
break;
}
}
isSuccessfulStatus() 必须根据网关的实际响应结构实现,不能只检查返回值是否为空。例如:
function isSuccessfulStatus($status)
{
return is_array($status)
&& isset($status['status'])
&& $status['status'] === 'SUCCESS';
}
如果文档明确要求无论结果如何都至少调用三次,就不能提前 break。
检查 PHP 和服务器超时
把 NuSOAP 超时设为 300 秒,并不能保证 PHP 进程一定能运行这么久。以下限制也必须大于 300 秒,而且要为后续三次状态查询留出足够时间:
- PHP 的
max_execution_time; - PHP-FPM 的
request_terminate_timeout; - Web 服务器或反向代理的请求超时;
- 负载均衡器、容器平台或任务执行器的超时;
- 调用
getStatus()时使用的网络超时。
如果 PHP 进程正好在第 300 秒被终止,后面的 getStatus() 就无法执行。支付业务更适合放到队列消费者或后台任务中,通过持久化状态和补偿任务继续查询,而不是依赖一次 HTTP 请求存活数分钟。
关键注意事项
支付超时表示结果未知,并不表示支付失败。可靠的处理流程如下:
- 调用前生成唯一且可重复查询的交易号。
- 将交易记录保存为“处理中”。
- 调用
doPayment()。 - 收到明确结果后更新本地状态。
- 发生超时或通信错误时,使用原交易号调用
getStatus()。 - 查询三次后仍无法确定状态,则保留为“未知/待确认”,由后台任务继续查询。
- 除非网关明确支持幂等请求,否则不要再次调用
doPayment()。
response_timeout = 300 通常限制的是网络读取时间,不能保证整个操作的总墙钟时间被严格控制在 300 秒以内。如果网关要求在第 300 秒立即发起状态查询,应把支付调用放到独立工作进程中,再由另一个调度任务根据持久化的开始时间触发 getStatus()。当单个同步 NuSOAP 调用仍处于阻塞状态时,它无法同时执行状态查询。
备注:内容仅供参考。
