Symfony Mercure HTTPS 证书验证失败问题的完整解决方案

本文详解 symfony 应用通过 hubinterface 向本地 caddy mercure hub 推送更新时出现 “failed to send an update” 错误的根本原因(ssl 证书验证失败),并提供安全、可落地的配置修复方案,涵盖开发环境适配与生产注意事项。

在本地开发环境中使用 Symfony 集成 Mercure(尤其是搭配 Caddy 作为 Hub)时,常见报错如下:

Failed to send an update
SSL certificate problem: unable to get local issuer certificate for "https://localhost/.well-known/mercure"

或 PHP 原生 file_get_contents() 调用失败:

Warning: file_get_contents(https:

//localhost/.well-known/mercure): Failed to open stream: operation failed

该问题并非 Mercure 配置错误,而是 PHP HTTP 客户端(如 Symfony HttpClient 或 cURL)在发起 HTTPS 请求时,严格校验 TLS 证书链,而本地 Caddy 开发模式默认使用自签名证书(或未正确配置信任根证书),导致 SSL 握手失败。值得注意的是,终端 curl 命令能成功,是因为系统 curl 默认不启用严格的证书验证(或已配置 --insecure / curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false)),而 PHP 的 ext-curl 或 stream 上下文默认开启验证。

推荐解决方案(开发环境)

在 config/packages/framework.yaml 中为 Symfony HttpClient 显式禁用证书验证(仅限开发):

# config/packages/framework.yaml
framework:
    http_client:
        default_options:
            verify_peer: false
            # 可选:同时禁用主机名验证(若证书 CN 不匹配 localhost)
            verify_host: false
⚠️ 注意:verify_peer: false 仅应在本地开发环境启用。切勿在生产环境使用,否则将导致中间人攻击(MITM)风险。

? 其他补充排查与优化建议

  • 确认 Caddy 是否真正监听 HTTPS:检查 Caddyfile.dev 是否包含 https://localhost 或 :443,并确保 Mercure Hub 启动日志显示 HTTPS server started on https://localhost:443。若仅监听 HTTP(如 http://localhost:3000),应统一使用 http:// 协议访问 .well-known/mercure,并在 Symfony 配置中设置 MERCURE_PUBLISH_URL=http://localhost/.well-known/mercure(需同时禁用 https 强制重定向)。

  • PHP cURL 扩展需启用:Symfony HttpClient 默认优先使用 cURL。运行 php -m | grep curl 确认已加载;若未启用,需在 php.ini 中取消 ;extension=curl 注释。

  • 纯 PHP 示例修复:若仍需使用 file_get_contents(),需显式配置上下文:

    $context = stream_context_create([
        'http' => [
            'method' => 'POST',
            'header' => "Authorization: Bearer {$jwt}\r\nContent-Type: application/x-www-form-urlencoded\r\n",
            'content' => http_build_query(['topic' => $topic, 'data' => $data]),
            'ignore_errors' => true,
        ],
        'ssl' => [
            'verify_peer' => false,
            'verify_peer_name' => false,
        ],
    ]);
    $result = file_get_contents('https://localhost/.well-known/mercure', false, $context);
  • 终极调试技巧:启用 HttpClient 日志,定位真实请求细节:

    bin/console debug:container --parameter=framework.http_client.default_options

    并在代码中捕获异常详情:

    try {
        $hub->publish(new Update(['https://example.com/books/1'], '{"foo":"bar"}'));
    } catch (TransportException $e) {
        dump($e->getPrevious()?->getMessage()); // 查看底层 cURL 错误
    }

? 总结
“Failed to send an update” 的核心症结是 PHP 客户端对本地 Mercure Hub 自签名 HTTPS 证书的严格校验。通过在 framework.yaml 中配置 verify_peer: false 可快速解决开发问题。但请始终牢记:此配置是开发便利性让步,上线前必须切换为受信证书(如 Let’s Encrypt)并恢复证书验证,保障通信安全。