使用PHP+Guzzle调用Microsoft Graph API大文件分片上传失败如何解决?
首先,你遇到的cURL 56错误(SSL_ERROR_SYSCALL errno 54)本质上是服务器主动断开了连接,根源是你的请求参数不符合Graph API的要求,尤其是最后一个分片的请求头和数据不匹配。下面是具体问题和修复方案:
核心错误:Content-Range计算错误
看你的代码,这里有个关键的计算错误:
$end = $chunkSize + $start; if ($end > $fileNbByte) { $end = $fileNbByte; } // 然后在Content-Range里写的是: 'Content-Range' => "bytes " . $start . "-" . ($end - 1) . "/" . $fileSize,
因为$fileNbByte = $fileSize - 1(字节索引从0开始),当处理最后一个分片时,$end已经是文件的最后一个字节索引(比如50MB文件的最后一个字节是52428799),但你又减了1,导致Content-Range的结束位置少了一个字节。这会让服务器认为你发送的字节数和声明的范围不匹配,直接断开连接,引发SSL错误。
修复方式:把Content-Range中的($end - 1)改成$end,确保范围和实际发送的字节数一致:
'Content-Range' => "bytes " . $start . "-" . $end . "/" . $fileSize,
次要问题:最后一个分片的响应类型错误
Graph API的分片上传中,前几个分片返回的是UploadSession对象,但最后一个分片上传成功后,返回的是DriveItem对象(即上传完成的文件信息)。你统一用setReturnType(Model\UploadSession::class)处理所有响应,会导致最后一次请求解析响应失败,进一步加剧连接问题。
修复方式:在上传前判断是否是最后一个分片,如果是,就不要设置返回类型,或者单独处理响应:
// 在循环内添加判断 $isLastChunk = ($end === $fileNbByte); try { $request = $graph->createRequest("PUT", $uploadSession->getUploadUrl()) ->addHeaders([ 'Connection' => "keep-alive", 'Content-Length' => strlen($bytes), // 改用实际读取的字节数更准确 'Content-Range' => "bytes " . $start . "-" . $end . "/" . $fileSize, 'Accept' => "*/*", // 'Origin' 可以去掉,除非你的场景必须指定 ]) ->attachBody($bytes); if (!$isLastChunk) { $res = $request->setReturnType(Model\UploadSession::class)->execute(); } else { // 最后一个分片,返回DriveItem $res = $request->setReturnType(Model\DriveItem::class)->execute(); echo "上传完成!文件ID:" . $res->getId() . "\n"; } } catch (GraphException $e) { echo "分片上传失败:" . $e->getMessage() . "\n"; }
其他优化点
文件读取逻辑优化:你用
$fgetsLength = $chunkSize + 1是多余的,fread($handle, $chunkSize)会自动在文件末尾返回剩余的字节,不需要额外加1,直接用$chunkSize作为读取参数即可。Content-Length更准确:用
strlen($bytes)代替$end - $start,确保发送的字节数和声明的长度完全一致,避免因文件读取异常导致的不匹配。去掉不必要的请求头:
Origin头如果不是你的场景必须的,可以去掉,减少潜在的请求头问题。
修正后的完整核心循环代码
$file = __DIR__ . '/file_50mb.dat'; $handle = fopen($file, 'r'); $fileSize = filesize($file); $fileNbByte = $fileSize - 1; // 最后一个字节的索引 $chunkSize = 327680 * 16; // 5MB,符合Graph API的分片要求(320KB倍数) $start = 0; while (!feof($handle)) { $bytes = fread($handle, $chunkSize); $bytesLength = strlen($bytes); if ($bytesLength === 0) break; // 避免空分片 $end = $start + $bytesLength - 1; // 直接根据读取的字节数计算结束位置 if ($end > $fileNbByte) { $end = $fileNbByte; } $isLastChunk = ($end === $fileNbByte); echo "start: " . $start . "\n"; echo "end: " . $end . "\n"; echo "bytes " . $start . "-" . $end . "/" . $fileSize . "\n"; echo "chunk size: " . $bytesLength . " bytes, " . ($bytesLength * 0.000001) . " MB" . "\n\n"; try { $request = $graph->createRequest("PUT", $uploadSession->getUploadUrl()) ->addHeaders([ 'Connection' => "keep-alive", 'Content-Length' => $bytesLength, 'Content-Range' => "bytes " . $start . "-" . $end . "/" . $fileSize, 'Accept' => "*/*", ]) ->attachBody($bytes); if (!$isLastChunk) { $res = $request->setReturnType(Model\UploadSession::class)->execute(); } else { $res = $request->setReturnType(Model\DriveItem::class)->execute(); echo "上传成功!文件信息:" . json_encode($res) . "\n"; } } catch (GraphException $e) { echo "上传失败:" . $e->getMessage() . "\n"; echo "请求详情:" . $e->getRequest() . "\n"; } $start = $end + 1; // 下一个分片的起始位置是当前结束位置+1 } fclose($handle);
总结
你的问题主要是两个点:Content-Range的结束位置计算错误,导致请求不匹配被服务器断开;最后一个分片的响应类型处理错误,引发解析异常。修复这两个问题后,应该就能正常完成大文件的分片上传了。
内容的提问来源于stack exchange,提问作者David Cotto

