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

SharePoint REST API上传>200MB内存文件报SPBITSSessionIncompleteException

错误触发核心原因

SPBITSSessionIncompleteException是SharePoint分片上传BITS会话校验失败抛出的错误,代码存在3个直接导致大文件内存上传失败的问题:

  • 文件大小计算完全错误:使用BytesIO.__sizeof__()获取的是BytesIO对象自身的内存占用,不是流内存储的CSV内容的真实字节长度,大文件场景下该值和实际内容长度偏差极大,会导致分片逻辑错乱,最终上传的总字节数和服务端预期值不匹配,会话校验直接失败。本地文件上传时使用的是文件系统返回的真实文件大小,所以不会触发该问题。
  • 分片上传流程不符合API规范:StartUpload接口本身要求传入第一个分片的字节内容,原代码调用StartUpload时没有传入任何请求体,相当于第一个分片上传了0字节,后续偏移量计算全部错位,小文件场景下服务端可能容错解析,大文件下会直接判定会话不完整。
  • 偏移量完全本地计算,没有使用服务端返回的确认值:SharePoint每个分片上传成功后会在响应中返回已确认接收的真实偏移量,本地自行计算偏移量很容易因为字节读取长度偏差、网络重试导致偏移不匹配。
修复步骤
  1. 替换文件大小计算逻辑:DataFrame写入BytesIO完成后,先将指针移到流末尾,通过tell()获取真实的内容字节长度,再将指针重置到起始位置准备读取。
  2. 修正分片上传流程:StartUpload传入第一个分片,后续分片根据当前偏移量判断调用ContinueUpload(中间分片)还是FinishUpload(最后一个分片)。
  3. 每传完一个分片,从响应结果中提取服务端返回的偏移量作为下一次上传的起始值,不要本地累加计算。
  4. 分片大小固定设置在100MB~150MB区间,不要超过SharePoint Online单分片250MB的硬阈值。
  5. 上传文件内容时指定Content-Type: application/octet-stream请求头,避免服务端错误解析请求体。
修复后可运行代码片段
import io
import uuid
import requests
import pandas as pd

# 固定分片大小为100MB,规避单分片大小阈值问题
CHUNK_SIZE = 100 * 1024 * 1024

# 1. 生成CSV内存流
output = io.BytesIO()
df.to_csv(output, **kwargs)
# 写完后将指针移到末尾,获取真实文件大小
output.seek(0, io.SEEK_END)
file_size = output.tell()
# 指针移回起始位置准备读取
output.seek(0)

upload_id = str(uuid.uuid4())
file_full_path = f"/sites/{self._site_name}/{lib_name}/{folder_path}/{file_name}"

# 2. 预创建空文件
create_url = f"{self._URL}/_api/web/GetFolderByServerRelativeUrl('{lib_name}/{folder_path}')/Files/add(url='{file_name}',overwrite=true)"
temp_headers = headers.copy()
temp_headers["Content-Type"] = "application/json;odata=verbose"
response = requests.post(create_url, headers=temp_headers, proxies=self._proxies)
response.raise_for_status()

# 3. 分片上传流程
current_offset = 0
upload_headers = headers.copy()
upload_headers["Content-Type"] = "application/octet-stream"

# 上传第一个分片
first_chunk = output.read(CHUNK_SIZE)
start_url = f"{self._URL}/_api/web/GetFileByServerRelativePath(DecodedUrl='{file_full_path}')/StartUpload(uploadId=guid'{upload_id}')"
response = requests.post(
    start_url,
    headers=upload_headers,
    data=first_chunk,
    proxies=self._proxies
)
response.raise_for_status()
current_offset = response.json()['d']['StartUpload']

# 上传剩余分片
while current_offset < file_size:
    chunk = output.read(CHUNK_SIZE)
    # 判断是否为最后一个分片
    if current_offset + len(chunk) >= file_size:
        req_url = f"{self._URL}/_api/web/GetFileByServerRelativePath(DecodedUrl='{file_full_path}')/FinishUpload(uploadId=guid'{upload_id}',fileOffset={current_offset})"
    else:
        req_url = f"{self._URL}/_api/web/GetFileByServerRelativePath(DecodedUrl='{file_full_path}')/ContinueUpload(uploadId=guid'{upload_id}',fileOffset={current_offset})"
    
    response = requests.post(
        req_url,
        headers=upload_headers,
        data=chunk,
        proxies=self._proxies
    )
    response.raise_for_status()
    # 用服务端返回的偏移量更新本地值,不自行累加
    resp_data = response.json()['d']
    current_offset = resp_data.get('ContinueUpload', resp_data.get('FinishUpload'))

output.close()
额外排查方向

如果修复后仍然存在200MB以上文件上传失败的问题,按以下顺序排查:

  • 校验X-RequestDigest有效期:SharePoint生成的请求摘要默认有效期30分钟,如果上传大文件耗时超过该时长,需要中途刷新摘要值。
  • 检查代理配置:企业内网代理通常会对超过200MB的POST请求体做截断或超时处理,可临时绕过代理测试确认。
  • 关闭requests的自动重定向跟随:分片上传接口返回30x状态码时直接抛出错误,不要自动跟随重定向导致分片丢失。
  • 给所有请求加状态码校验,每一步请求失败直接抛出异常,不要继续后续上传流程。

内容的提问来源于stack exchange,提问作者Matthias

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 19:09:23