本地与服务器上Guzzle/PHP调用FCM出现411 Content-Length异常
我在本地用PHP+Guzzle向FCM发送请求完全正常,但部署到生产服务器(Docker环境)后,FCM一直返回错误:411 Content-Length required。
已经确认过本地和生产环境的PHP版本、Guzzle版本、cURL版本完全一致,Docker镜像配置也相同(Dockerfile如下)。尝试手动添加Content-Length头也没有效果。
相关代码
Guzzle实现
$handler = new CurlHandler(); $stack = HandlerStack::create($handler); $config['handler'] = $stack; $client = new Client($config); try { $client->post($this->apiUrl, [ RequestOptions::JSON => $data, RequestOptions::HEADERS => [ 'Authorization' => sprintf('key=%s', "key"), ] ]); } catch (TransferException $e) { throw new \RuntimeException($e->getMessage(), $e->getCode(), $e); }
尝试原生cURL(exec方式)
$curl = sprintf("curl -i -X POST https://fcm.googleapis.com/fcm/send -H 'Authorization: key=%s' -H 'Content-Type: application/json' -d '%s'", $message->getSender(), $data); exec($curl, $output);
用原生方式测试结果一样:在生产机器的bash里直接运行curl命令正常,但通过PHP的exec执行就返回411错误。
Dockerfile配置
FROM php:7.1-fpm-alpine RUN apk --update --no-cache add libmcrypt-dev libmcrypt curl-dev libcurl \ && docker-php-source extract \ && docker-php-source delete \ && docker-php-ext-install mcrypt pdo_mysql mbstring curl RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer RUN composer --version RUN chmod +x /usr/local/bin/composer WORKDIR /var/www/project
想请教:
- 这个问题可能的原因是什么?
- 有没有办法在本地复现这个问题?
这个411错误的核心是FCM服务器未接收到有效的Content-Length头,或者请求体传输异常,结合你的环境和测试情况,大概率是以下几个原因:
1. 生产环境网络中间件修改请求头
很多生产环境会部署反向代理(如Nginx、HAProxy)或防火墙,这些中间件可能会剥离或修改请求头——尤其是当Guzzle默认使用chunked编码传输请求体时(Guzzle在无法确定Content-Length时会自动用chunked),FCM对这种编码的兼容性可能存在问题,或者中间件转发时丢失了关键头信息。
解决办法:强制禁用chunked编码,手动控制请求体和头
直接序列化JSON数据,手动计算Content-Length,避免Guzzle自动处理带来的不确定性:
// 先序列化JSON数据,确保内容可控 $jsonData = json_encode($data); $client->post($this->apiUrl, [ RequestOptions::BODY => $jsonData, RequestOptions::HEADERS => [ 'Authorization' => sprintf('key=%s', "your_fcm_key"), 'Content-Type' => 'application/json', 'Content-Length' => strlen($jsonData) ], // 禁用chunked编码,强制使用HTTP/1.1 'curl' => [ CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, CURLOPT_TRANSFER_ENCODING => false, ] ]);
2. PHP cURL配置被环境异常覆盖
生产环境的php.ini或Docker镜像可能存在特殊配置,比如curl.cainfo未正确设置导致SSL握手异常(虽然错误是411,但SSL异常可能导致请求体传输中断,服务器返回非预期错误),或者执行时间、内存限制导致请求未完整发送。
解决办法:强制指定SSL证书路径
Alpine镜像的CA证书默认路径是/etc/ssl/certs/ca-certificates.crt,可以在Guzzle配置里强制指定:
'curl' => [ CURLOPT_CAINFO => '/etc/ssl/certs/ca-certificates.crt', CURLOPT_SSL_VERIFYPEER => true, ]
3. exec执行curl时的字符串转义问题
你用sprintf拼接curl命令时,如果$data包含单引号、空格等特殊字符,会导致命令被截断,实际发送的请求体为空,自然触发411错误。
解决办法:用escapeshellarg转义所有参数
$authValue = escapeshellarg(sprintf('key=%s', $message->getSender())); $postData = escapeshellarg($data); $curl = "curl -i -X POST https://fcm.googleapis.com/fcm/send -H 'Authorization: $authValue' -H 'Content-Type: application/json' -d $postData"; exec($curl, $output, $returnCode); // 可以打印返回码和输出排查问题 var_dump($returnCode, $output);
如何在本地复现问题?
你可以模拟生产环境的异常场景:
- 搭反向代理:在本地用Nginx做反向代理转发FCM请求,配置Nginx剥离
Content-Length头,测试是否触发411错误。 - 模拟异常配置:修改本地PHP的cURL配置,比如禁用chunked编码,或者设置错误的CA证书路径,模拟生产环境的异常配置。
- 完全复刻生产Docker环境:在本地运行和生产一致的Docker镜像,同时模拟生产的网络策略(比如添加内网代理),看是否能复现问题。
内容的提问来源于stack exchange,提问作者user9669681

