PAYATHON 2026

如何在 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 请求存活数分钟。

关键注意事项

支付超时表示结果未知,并不表示支付失败。可靠的处理流程如下:

  1. 调用前生成唯一且可重复查询的交易号。
  2. 将交易记录保存为“处理中”。
  3. 调用 doPayment()。
  4. 收到明确结果后更新本地状态。
  5. 发生超时或通信错误时,使用原交易号调用 getStatus()。
  6. 查询三次后仍无法确定状态,则保留为“未知/待确认”,由后台任务继续查询。
  7. 除非网关明确支持幂等请求,否则不要再次调用 doPayment()。

response_timeout = 300 通常限制的是网络读取时间,不能保证整个操作的总墙钟时间被严格控制在 300 秒以内。如果网关要求在第 300 秒立即发起状态查询,应把支付调用放到独立工作进程中,再由另一个调度任务根据持久化的开始时间触发 getStatus()。当单个同步 NuSOAP 调用仍处于阻塞状态时,它无法同时执行状态查询。

备注:内容仅供参考。