【已记录】Bug: WebDAV 存储插件自动备份上传必现 403(Depth:inf
环境项目版本/值
面板宝塔 / aaPanel 11.4.0
插件WebDav存储 (webdav) v1.3,2024-06-05
插件文件/www/server/panel/plugin/webdav/webdav_main.py
Python3.7(面板自带 pyenv)
webdav40.9.8
httpx0.24.1
系统Oracle Linux Server 8.8, aarch64
WebDAV 服务端自建(域名经 Cloudflare 代理),Apache mod_dav
现象计划任务里配置的"数据库全量备份 + WebDAV 云端同步",本地备份和压缩包生成都正常,但同步到 WebDAV 这一步必现失败:
httpx.HTTPStatusError: Client error '403 Forbidden' for url 'https://webdav.xxx.com/home'
...
webdav4.client.HTTPError: received 403 (Forbidden)
File ".../webdav_main.py", line 133, in ensure_directories_exist
if not self.client.exists(current_path):
File ".../webdav_main.py", line 175, in upload_file_in_chunks
if not self.ensure_directories_exist(remote_dir):
File ".../class-projectModel-binlogModel.py", line 844, in execute_by_comandline
if self._cloud.upload_file(zip_file, upload_path):
面板计划任务日志把这次备份仍标记为 ★ ... Successful,因为云端上传失败没有被上层捕获成"失败"状态,只是在 stderr 里打了一段未处理的 Python traceback——对用户来说观感是"备份成功了",但云端其实完全没同步上,属于静默失败。根本原因webdav4.client.Client.exists() 内部实现(0.9.8):
def exists(self, path: str) -> bool:
try:
self.propfind(path) # 不带 headers,Depth 缺省 = "infinity"
except ResourceNotFound:
return False
return True
propfind() 不传 headers 时不会设置 Depth,按 WebDAV 规范(RFC 4918)省略 Depth 等价于 Depth: infinity。而本例 WebDAV 服务端(Apache mod_dav)对 /home 这一层显式禁止无限深度的 PROPFIND,直接返回:
HTTP/1.1 403 Forbidden
<title>403 Forbidden</title>
<h1>Forbidden</h1>
<p>PROPFIND requests with a Depth of "infinity" are not allowed for /home.</p>
插件里所有目录/文件存在性检查都是直接调用 self.client.exists(path):
[*]create_directory()(连接时初始化远程目录)
[*]ensure_directories_exist()(每次上传前逐级建目录)
[*]upload_file_in_chunks() 分块上传循环里判断分块文件是否已存在
这三处全部会命中同一个 403,且是确定性复现,不是网络抖动——用 curl 手动测试时如果显式带 -H "Depth: 0" 反而永远成功,这也是最初排查时误判成"Cloudflare 瞬时故障"的原因:两种客户端发的请求本质不同(有没有 Depth 头),不是同一个请求的两次结果。复现步骤
[*]配置任意一个真实拒绝 Depth: infinity PROPFIND 的 WebDAV 服务端做云端存储(Apache 默认较新版本、部分 NAS/群晖 WebDAV 实现都有此限制)。
[*]触发一次全量备份并勾选/启用云端同步。
[*]ensure_directories_exist() 第一次调用 self.client.exists() 即抛出未捕获的 HTTPError,整条上传链路中断,日志里只看到裸 Python traceback,没有任何面向用户的错误提示。
影响
[*]云端备份完全没有落地,且面板任务日志仍显示整体"Successful",容易被误认为一切正常,只有等真正需要恢复时才会发现云端是空的。
[*]影响所有三个存在性检查点(连接初始化建目录 / 每次上传前建目录 / 分块存在性判断),不是偶发个例。
[*]任何 WebDAV 服务端只要对无限深度 PROPFIND 有安全限制(这是比较常见的加固配置),都会 100% 触发本问题,不限于本环境这一家。
建议修复不要依赖 webdav4.Client.exists(),改为显式指定 Depth: 0 的浅层 PROPFIND:
from webdav4.client import ResourceNotFound
def _path_exists(self, path):
try:
self.client.propfind(path, headers={'Depth': '0'})
except ResourceNotFound:
return False
return True
并把 webdav_main.py 里所有 self.client.exists(...) 的调用替换为 self._path_exists(...)(create_directory、ensure_directories_exist、upload_file_in_chunks 分块循环,共 4 处)。同时建议:
[*]ensure_directories_exist() / 分块上传对单次请求失败增加有限重试(真实网络抖动仍然存在,跟本 bug 是两回事,但顺手能一起做),而不是遇错直接把异常整个抛出中断整条上传链路。
[*]execute_by_comandline()(binlogModel.py)在云端上传失败时不应仍然整体标记为 Successful,至少要在日志里给出一条用户能看懂的失败提示,而不是裸露 Python traceback。
本地临时修复(已自测验证,仅个人环境临时用,非官方 patch)已在本机 /www/server/panel/plugin/webdav/webdav_main.py 按上述方案打了 patch(原文件备份于 /root/plugin-backups/webdav_main.py.bak.20260922_172106),验证方式:
[*]用面板同款 pyenv 解释器加载补丁后的插件,ensure_directories_exist() 连续 3 次全部返回 True(补丁前 100% 复现 403)。
[*]跑了一次完整的分块上传(写测试文件 → upload_file_in_chunks() → 确认远端存在 → 删除清理),全程走插件自身代码路径。
⚠️ 该 patch 是直接改的面板系统文件,下次插件更新会被覆盖,仅作为临时缓解,最终还是需要官方修复。
您好,非常感谢反馈和支持!您的建议已收到并记录,后续会有相关人员进行排障修复
如果您还有更多问题和疑虑,您可以通过客服咨询宝塔面板相关问题:https://docs.bt.cn/wechat-service
操作方式:复制链接在微信打开,即可在线咨询客服。
页:
[1]