PAYATHON 2026

CURLINFO_HTTP_CODE 偶尔无法获取 HTTP 状态码

支付老李

结论

CURLINFO_HTTP_CODE 不会把已经收到的 200 随机变成 null。如果客户端没有完整收到 HTTP 响应头,它通常会返回整数 0。如果 $statusCode 是 null 或空字符串,更可能是应用代码没有执行赋值语句、句柄已经关闭、变量被覆盖,或者日志序列化改变了原始值。

网关完成扣款,只能说明服务端已经处理了交易,不能说明客户端成功收到了返回的 200。支付请求可能处于“不确定状态”:服务端已经成功扣款,但响应在返回途中丢失,或者客户端处理响应时失败。

为什么会出现这种情况

服务端已扣款,但客户端没有收到响应头

一次请求可能经历以下过程:

  1. 网关接收请求。
  2. 网关完成扣款。
  3. 网关准备返回 HTTP 200。
  4. 网络连接、代理连接或 HTTP/2 流在响应到达客户端前中断。

这种情况下,网关记录的是成功,客户端却拿不到 HTTP 状态码。即使没有出现 CURLE_OPERATION_TIMEDOUT,仍可能发生其他错误,例如连接重置、响应解析失败、写入回调失败或 HTTP/2 流错误。

所以,不能只检查请求是否超时,还要查看完整的 cURL 错误信息。

curl_exec() 失败没有被正确区分

启用 CURLOPT_RETURNTRANSFER 后:

  • 请求成功但响应体为空时,curl_exec() 可能返回空字符串 ''。
  • cURL 执行失败时,curl_exec() 返回布尔值 false。

不要这样判断:

if (!$response) {
    // 无法区分 false、''、'0'
}

应该使用严格比较:

if ($response === false) {
    // cURL 执行失败
}

HTTP 200 可以没有响应体,因此响应体为空不代表请求失败。

获取状态码的时机不正确

应该在 curl_exec() 之后、curl_close() 之前调用 curl_getinfo():

$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

如果在请求执行前读取状态码、关闭句柄后再读取,或者某个异常分支跳过了赋值,结果就不可靠。

应用代码覆盖了变量

正常情况下,下面的返回值应该是整数。无法取得响应码时,返回值通常是 0:

$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

如果日志中出现 null,需要检查:

  • $statusCode 是否只在部分分支中赋值;
  • 是否在 finally、异常处理或重试逻辑中被重置;
  • 并发任务是否复用了同一变量或同一个 cURL handle;
  • 日志系统是否忽略了 0、false 或空字符串;
  • 是否使用了 empty($statusCode),把整数 0 当成空值;
  • 是否在未检查 curl_getinfo() 返回值的情况下直接访问数组或进行类型转换。

推荐的请求与日志写法

<?php

$ch = curl_init($url);

curl_setopt_array($ch, [
    CURLOPT_POST            => true,
    CURLOPT_POSTFIELDS      => json_encode($payload),
    CURLOPT_HTTPHEADER      => [
        'Content-Type: application/json',
        'Accept: application/json',
        'Idempotency-Key: ' . $idempotencyKey,
    ],
    CURLOPT_RETURNTRANSFER  => true,
    CURLOPT_CONNECTTIMEOUT  => 10,
    CURLOPT_TIMEOUT         => 30,
]);

$response = curl_exec($ch);

/*
 * 必须在 curl_close() 前保存这些信息。
 */
$curlErrno  = curl_errno($ch);
$curlError  = curl_error($ch);
$curlInfo   = curl_getinfo($ch);
$statusCode = (int) ($curlInfo['http_code'] ?? 0);

curl_close($ch);

$logData = [
    'transaction_id' => $transactionId,
    'idempotency_key' => $idempotencyKey,
    'curl_errno'      => $curlErrno,
    'curl_error'      => $curlError,
    'http_code'       => $statusCode,
    'response_type'   => gettype($response),
    'response_length' => is_string($response) ? strlen($response) : null,
    'response'        => is_string($response) ? $response : null,
];

if ($response === false) {
    /*
     * 这是传输失败,但不能据此认定支付失败。
     * 网关可能已经完成扣款。
     */
    markTransactionAsUnknown($transactionId, $logData);
} elseif ($statusCode === 200) {
    markTransactionAsSuccessful($transactionId, $response);
} elseif ($statusCode === 0) {
    /*
     * 没有收到可用的 HTTP 响应码。
     */
    markTransactionAsUnknown($transactionId, $logData);
} else {
    handleGatewayResponse($transactionId, $statusCode, $response);
}

如果 PHP 和 libcurl 的版本支持,也可以使用 CURLINFO_RESPONSE_CODE。为兼容现有环境,继续使用 CURLINFO_HTTP_CODE 也没有问题。

记录原始响应头

要确认客户端是否真的收到了状态行,可以通过 header callback 单独记录响应头:

<?php

$responseHeaders = [];

curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($ch, string $headerLine) use (&$responseHeaders): int {
    $responseHeaders[] = $headerLine;
    return strlen($headerLine);
});

$response = curl_exec($ch);
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErrno = curl_errno($ch);
$curlError = curl_error($ch);

重定向、代理握手和认证过程可能产生多组响应头,因此不能简单地把第一行视为最终状态码。最终状态仍应以 curl_getinfo() 为准,原始响应头主要用于排查问题。

生产日志至少应保存:

[
    'http_code'       => curl_getinfo($ch, CURLINFO_HTTP_CODE),
    'curl_errno'      => curl_errno($ch),
    'curl_error'      => curl_error($ch),
    'effective_url'   => curl_getinfo($ch, CURLINFO_EFFECTIVE_URL),
    'total_time'      => curl_getinfo($ch, CURLINFO_TOTAL_TIME),
    'primary_ip'      => curl_getinfo($ch, CURLINFO_PRIMARY_IP),
    'response_length' => is_string($response) ? strlen($response) : null,
]

还应记录网关交易号、本地交易号、幂等键和请求时间,但不要将银行卡号、CVV、完整身份信息或密钥写入日志。

支付场景不能只依赖 HTTP 200

HTTP 状态码表示本次通信的结果,不能作为判断支付状态的唯一依据。更稳妥的处理方式是:

  1. 为每笔支付生成唯一的本地交易号。
  2. 如果网关支持幂等键,同一笔业务交易应始终使用同一个键。
  3. 收到 200 后,还要校验响应体中的业务状态和交易号。
  4. 没有收到状态码、连接中断或响应无法解析时,将交易标记为 unknown 或 pending。
  5. 调用网关的交易查询接口确认最终状态。
  6. 通过 webhook 或异步通知校准状态。
  7. 定期对账,处理客户端记录与网关记录不一致的交易。

没有取得 200 时,绝不能直接换一个新的交易号再次扣款。只有网关明确支持幂等请求,或者已经查询并确认原交易失败,才能安全重试。

最需要检查的代码点

先确认实际记录的是下面三个不同的值:

var_dump($response);
var_dump(curl_errno($ch));
var_dump(curl_getinfo($ch, CURLINFO_HTTP_CODE));

它们分别表示响应体、cURL 错误码和 HTTP 状态码,不能相互替代。

如果 curl_errno() 不为零,问题就不是“状态码偶尔消失”,而是发生了传输或处理错误。如果 curl_errno() 为 0、HTTP 状态码为 0,并且响应头确实为空,就需要继续检查代理、负载均衡器、HTTP/2 连接和 cURL 回调。如果 $statusCode 确实是 null,而不是整数 0,排查重点应放在应用的赋值、控制流和日志代码上。

备注:内容仅供参考。