You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用PHP+Guzzle调用Microsoft Graph API大文件分片上传失败如何解决?

分析与修复你的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";
}

其他优化点

  1. 文件读取逻辑优化:你用$fgetsLength = $chunkSize + 1是多余的,fread($handle, $chunkSize)会自动在文件末尾返回剩余的字节,不需要额外加1,直接用$chunkSize作为读取参数即可。

  2. Content-Length更准确:用strlen($bytes)代替$end - $start,确保发送的字节数和声明的长度完全一致,避免因文件读取异常导致的不匹配。

  3. 去掉不必要的请求头: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:59:19