PAYATHON 2026

payment gateway URL 出现 curl error no 35

支付阿杰

结论

curl 错误 35(CURLE_SSL_CONNECT_ERROR)表示 HTTPS 请求在 TLS/SSL 握手阶段失败。此时请求还没有进入表单提交或支付参数处理阶段。

浏览器可以访问,但 curl 失败,通常是因为两者使用的 TLS 实现或运行环境不同。常见原因包括:

  • PHP 使用的 libcurl、OpenSSL 或系统 TLS 库版本过旧;
  • 客户端和网关支持的 TLS 版本或密码套件不兼容;
  • 旧版 libcurl 不支持 SNI,或者没有正确发送 SNI;
  • curl 使用了浏览器未使用的代理;
  • 服务器的证书链、虚拟主机或 TLS 配置存在兼容性问题;
  • 浏览器和 PHP 实际运行在不同的机器、容器或网络出口。

CURLOPT_SSL_VERIFYPEER => false 只会关闭证书可信性校验,无法解决 TLS 版本、密码套件、SNI 或握手协议不兼容的问题,因此不能作为正式解决方案。

先确认 PHP 实际使用的 TLS 环境

浏览器能访问,只能说明浏览器所在的环境可以完成握手,并不代表运行 PHP 的服务器也可以。先查看 PHP 使用的 libcurl 和 SSL 库版本:

<?php

var_dump(curl_version());

重点查看:

  • version:libcurl 版本;
  • ssl_version:OpenSSL、LibreSSL、NSS、GnuTLS 或其他 TLS 后端;
  • protocols:是否包含 https。

还要确认命令行 PHP 和 Web 服务器中的 PHP 是否加载了相同配置。两者可能使用不同的 PHP 版本或 php.ini。

查看握手的详细错误

可以临时开启 verbose 日志,并把日志写入内存。排查真实支付请求时,必须隐藏卡号、密码和其他敏感信息。

<?php

$log = fopen('php://temp', 'w+');

$ch = curl_init('https://secure.msicharge.com/api/transact.php');

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_VERBOSE        => true,
    CURLOPT_STDERR         => $log,
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT        => 30,
]);

$response = curl_exec($ch);
$errorNo = curl_errno($ch);
$error   = curl_error($ch);
$info    = curl_getinfo($ch);

rewind($log);
$verboseLog = stream_get_contents($log);

curl_close($ch);
fclose($log);

var_dump($errorNo, $error, $info, $verboseLog);

查看日志时,重点确认以下内容:

  • 是否连接到了正确的 IP 和 443 端口;
  • 是否经过 HTTPS_PROXY 等代理;
  • TLS 握手在哪一步终止;
  • 是否出现 handshake failure、wrong version number、no shared cipher 或连接被重置;
  • 返回内容是否来自代理、防火墙或其他非 HTTPS 服务。

不要公开发布包含认证信息、卡号、Cookie 或请求正文的 verbose 日志。

如果可以登录 PHP 所在的服务器,还可以在同一环境中执行:

curl -v https://secure.msicharge.com/api/transact.php

以及:

openssl s_client \
  -connect secure.msicharge.com:443 \
  -servername secure.msicharge.com

-servername 用于发送 SNI。如果不带 SNI 时失败,带上后成功,通常说明旧客户端的 SNI 支持有问题,或者服务器的虚拟主机配置存在问题。

优先升级 TLS 组件

支付网关通常会禁用 SSLv2、SSLv3、TLS 1.0、TLS 1.1 和较弱的密码套件。如果 PHP 所在服务器使用的 libcurl 或 OpenSSL 太旧,客户端和服务端之间可能找不到共同支持的协议组合。

应优先升级:

  • PHP;
  • libcurl;
  • OpenSSL 或系统使用的其他 TLS 库;
  • CA 根证书包。

不要为了兼容旧环境而强制使用 SSLv3。它已经不适合支付场景,现代服务端通常也不会接受。

如果已经确认网关支持 TLS 1.2,可以在排查时明确要求使用 TLS 1.2:

curl_setopt($ch, CURLOPT_SSLVERSION, CURL_SSLVERSION_TLSv1_2);

通常更合适的做法是不固定协议,让更新后的 libcurl 自动协商双方支持的最高版本。只有确认了服务器要求和运行环境的支持情况后,才应设置 CURLOPT_SSLVERSION。

使用正确的证书校验方式

原代码关闭了证书验证:

curl_setopt($request, CURLOPT_SSL_VERIFYPEER, false);

这会削弱 HTTPS 的身份验证,而且通常无法解决错误 35。正式环境中应启用验证:

curl_setopt_array($ch, [
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,
]);

如果系统缺少 CA 根证书,应安装或更新系统 CA 包。确有需要时,也可以指定可信的 CA 文件:

curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');

该文件必须来自可信来源,并定期更新。不要使用随意下载或未经核验的证书包。

一个更完整的请求示例

<?php

$url = 'https://secure.msicharge.com/api/transact.php';

$params = [
    'type'     => 'sale',
    'username' => 'demo',
    'password' => 'password',
    'ccnumber' => '4111111111111111',
    'ccexp'    => '1013',
    'amount'   => '50',
];

$ch = curl_init($url);

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => http_build_query($params),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HEADER         => false,
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_SSL_VERIFYPEER => true,
    CURLOPT_SSL_VERIFYHOST => 2,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/x-www-form-urlencoded',
        'Accept: */*',
    ],
]);

$response = curl_exec($ch);

if ($response === false) {
    $errorNo = curl_errno($ch);
    $error   = curl_error($ch);

    curl_close($ch);

    throw new RuntimeException(
        sprintf('Gateway request failed: curl error %d: %s', $errorNo, $error)
    );
}

$statusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($statusCode < 200 || $statusCode >= 300) {
    throw new RuntimeException(
        sprintf('Gateway returned HTTP status %d', $statusCode)
    );
}

echo $response;

http_build_query() 会正确编码参数。虽然向 CURLOPT_POSTFIELDS 传入字符串通常已经会发起 POST 请求,但明确设置 CURLOPT_POST 可以让代码意图更清楚。

还需要检查的环境差异

如果升级后仍然失败,可以依次检查:

  1. PHP 服务器是否通过代理访问外网,并检查 HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY。
  2. 防火墙、WAF 或出口网关是否拦截了 TLS 握手。
  3. 浏览器和 PHP 服务器上的 DNS 是否解析到了不同地址。
  4. IPv4 和 IPv6 路径的表现是否不同。可以暂时使用 CURLOPT_IPRESOLVE 进行诊断。
  5. 网关是否限制来源 IP,或者要求将服务器出口 IP 加入白名单。
  6. 网关是否要求客户端证书。如果需要,应配置 CURLOPT_SSLCERT 和对应的私钥。
  7. 系统时间是否准确。时间严重偏差可能导致证书验证失败,不过这类问题通常会显示更明确的证书错误。
  8. 服务端是否根据 SNI 提供不同的证书或 TLS 配置。

例如,可以仅为诊断 IPv4 路径设置:

curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);

如果设置后请求成功,应继续检查 IPv6 DNS、路由或防火墙问题,不要用这个选项长期掩盖故障。

注意事项

错误 35 通常与提交参数是否正确无关,因为 TLS 握手发生在 HTTP POST 发送之前。即使卡号格式、有效期或账号密码有误,也应该等 TLS 连接建立后,再由网关返回业务错误。

调用支付接口时还要注意:

  • 不要在日志、异常页面或调试输出中记录完整卡号、密码和安全码;
  • 示例中的有效期 1013 已经过期,但这不会导致 TLS error 35;
  • 应根据网关当前的接入文档确认 TLS 要求、证书链、IP 白名单和请求格式;
  • 不要在生产环境中关闭证书验证;
  • 如果服务端只支持已经淘汰的 SSL/TLS 协议,应联系支付网关升级服务端,不要降低客户端的安全级别。

最常见的处理方式是先在运行 PHP 的同一台服务器上取得 verbose 握手日志,确认代理和 DNS,再升级 PHP、libcurl、TLS 库及 CA 证书。如果更新后的客户端仍然无法完成握手,需要把握手日志、出口 IP 和连接时间提供给支付网关的技术支持,请对方检查服务端 TLS 配置或访问控制日志。

备注:内容仅供参考。