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

