CURLINFO_HTTP_CODE 偶尔无法获取 HTTP 状态码
结论
CURLINFO_HTTP_CODE 不会把已经收到的 200 随机变成 null。如果客户端没有完整收到 HTTP 响应头,它通常会返回整数 0。如果 $statusCode 是 null 或空字符串,更可能是应用代码没有执行赋值语句、句柄已经关闭、变量被覆盖,或者日志序列化改变了原始值。
网关完成扣款,只能说明服务端已经处理了交易,不能说明客户端成功收到了返回的 200。支付请求可能处于“不确定状态”:服务端已经成功扣款,但响应在返回途中丢失,或者客户端处理响应时失败。
为什么会出现这种情况
服务端已扣款,但客户端没有收到响应头
一次请求可能经历以下过程:
- 网关接收请求。
- 网关完成扣款。
- 网关准备返回
HTTP 200。 - 网络连接、代理连接或 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 状态码表示本次通信的结果,不能作为判断支付状态的唯一依据。更稳妥的处理方式是:
- 为每笔支付生成唯一的本地交易号。
- 如果网关支持幂等键,同一笔业务交易应始终使用同一个键。
- 收到
200后,还要校验响应体中的业务状态和交易号。 - 没有收到状态码、连接中断或响应无法解析时,将交易标记为
unknown或pending。 - 调用网关的交易查询接口确认最终状态。
- 通过 webhook 或异步通知校准状态。
- 定期对账,处理客户端记录与网关记录不一致的交易。
没有取得 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,排查重点应放在应用的赋值、控制流和日志代码上。
备注:内容仅供参考。
