Description
聊天附件的文件名只要包含连续两个或以上普通半角空格(U+0020),上传凭证接口可以正常返回 200,但随后向腾讯 COS 执行的预签名 PUT 会稳定返回 403 SignatureDoesNotMatch。Web 最终只显示“上传失败”,用户容易误判为文件过大、格式不支持或网络异常。
该问题已由两个真实文件独立复现:
- 1,140,120 字节 DOCX:文件名包含连续 3 个和 4 个半角空格;
- 11,876,252 字节 PDF:纯 ASCII 文件名包含连续 2 个半角空格。
两个样本均满足以下 A/B 结果:文件字节完全不变,只把连续空格折叠成一个空格后,PUT 从 403 变为 200;改为短文件名同样返回 200。因此本问题与文件内容、文件大小上限、文件格式、中文字符以及对象 key 是否使用原始文件名无关。
进一步使用完全脱敏的合成文件验证了 PDF、DOCX、XLSX、TXT 四种格式。每种格式的成功/失败文件在该格式内字节及 SHA-256 完全相同,仅文件名中的空格数量不同:
| 格式 |
单个 U+0020 |
连续两个 U+0020 |
| PDF |
PUT 200 |
PUT 403 SignatureDoesNotMatch |
| DOCX |
PUT 200 |
PUT 403 SignatureDoesNotMatch |
| XLSX |
PUT 200 |
PUT 403 SignatureDoesNotMatch |
| TXT |
PUT 200 |
PUT 403 SignatureDoesNotMatch |
四个失败文件各执行 4 次新鲜凭证 + PUT,累计 16/16 均返回相同的 403;四个单空格对照累计 16/16 均返回 200。当前环境中的有界实测失败率为 100%。
字符边界矩阵另外覆盖了 25 类文件名。当前只确认连续两个及以上 U+0020 会触发同一 COS 签名错误;单个半角空格、Tab、全角空格 U+3000、不换行空格 U+00A0、窄不换行空格 U+202F、零宽空格 U+200B、中文、Emoji 及常见标点均在当前链路 PUT 200。扩展名后的尾随空格会更早返回凭证 400,U+007F 会更早被 HTTP 客户端拒绝,属于不同问题。
当前代码和运行时证据指向 Server/COS 预签名边界:
modules/file/api.go 的 BuildContentDisposition 会把原始 ASCII 空格保留在 filename="..." 中,同时生成 filename*;
modules/file/service_cos.go 将该 Content-Disposition 加入签名 Header,并调用 MinIO Go PresignHeader;
- MinIO Go v7.0.61 的 SigV4 canonical-header 实现使用
strings.Fields 处理 Header 值;
- octo-web 将凭证接口返回的
contentDisposition 原样用于 PUT;
- 浏览器抓包显示凭证返回值与 PUT Header 完全一致;使用相同新鲜预签名 URL、相同 Header、显式
Content-Length 和相同文件字节直接执行 PUT,也会得到相同 403,排除了浏览器独有的 Header 改写;
- 把连续空格改为一个空格后,同一链路立即返回 200。
因此可以确认故障发生在 Server 生成并签名文件名相关 Header、再由 COS 验签的边界。COS 不返回其内部 canonical string,所以“双方具体在哪一步形成不同 canonical value”属于根据源码和 A/B 结果得到的机制推断;403、触发条件及故障边界本身已经实证。
历史记录:Mininglamp-OSS/octo-web#348 仍处于开放状态,并已在前端排查后标记为跨仓阻塞。该 Issue 早期评论认为后端未加引号或未生成 RFC 5987 filename*,但 2026-08-13 的线上响应已经同时包含这两项,因此旧判断不能解释当前复现。本 Issue 提供当前 Server 代码、真实 COS 返回和可公开夹具,作为后端独立跟踪项。
Steps to Reproduce
- 下载本 Issue 最后一条评论中的公开脱敏测试夹具 ZIP 并解压,保留原始文件名。
- 在同一个 Octo Web 会话中上传
01-PASS-Octo upload test.pdf。
- 观察该文件上传成功。
- 上传字节完全相同的
02-FAIL-Octo upload test.pdf;Octo 与 upload 之间是两个普通半角空格。
- 观察文件卡片显示“上传失败”;重试仍失败。
- 可分别使用
03/04(DOCX)、05/06(XLSX)、07/08(TXT)重复上述 A/B;每一对均只有一个空格与两个空格的区别。
底层复现边界:对失败文件调用 /api/v1/file/upload/credentials 会得到 200;随后携带响应中的 Content-Type、Content-Disposition、准确 Content-Length 和文件字节执行 COS PUT,会得到 403 SignatureDoesNotMatch。
Expected Behavior
合法文件在大小和扩展名均允许的情况下,应能完成上传;文件名中连续的普通空格不应导致预签名 PUT 验证失败。字节相同的文件不应仅因文件名从一个空格变为两个空格而产生 200/403 的差异。
Actual Behavior
- 上传凭证请求:200;
- COS CORS preflight:200(浏览器路径已验证);
- COS PUT:403
SignatureDoesNotMatch;
- Web:只显示通用“上传失败,点击图标重试”;
- 多次重试不会恢复;
- 将连续空格折叠为一个空格或改为短文件名后,相同字节上传成功。
该故障会阻断附件消息的后续发送,因此 Bot/OpenClaw 等下游也无法收到附件消息。失败后文件卡片可能继续残留或在后续会话同步时重新出现,这是 octo-web 的另一个失败队列生命周期现象,不是本 Server Issue 的 403 根因。
DMWork Version
线上环境,具体 DMWork 构建号未知;2026-08-13 仍稳定复现。相关用户曾提供 OpenClaw 包版本 2026.7.1-2,但 403 发生在附件上传阶段,与 OpenClaw 处理无关。
Affected Component
Backend (Server)
Severity
P1 - Core Feature Broken
Reproducibility
Environment
- Octo Web:
https://im.deepminer.com.cn/
- 验证日期:2026-08-13
- 存储服务:Tencent COS
- Server 源码基线:
Mininglamp-OSS/octo-server main,2aee19bbf136d32e55411e02f79d7367d475ea37
- MinIO Go:
github.com/minio/minio-go/v7 v7.0.61
- Web 源码基线:
Mininglamp-OSS/octo-web upstream/main,51bc8b3ee16209d226d3daad2947b14d137758f1
- 浏览器与直接 HTTP PUT 均可复现;不是仅浏览器出现的错误
Logs / Screenshots
失败链路的脱敏结果:
credentials: 200
OPTIONS: 200
PUT: 403
COS Code: SignatureDoesNotMatch
重复性结果:
双空格失败组:PDF/DOCX/XLSX/TXT,累计 16/16 → 403 SignatureDoesNotMatch
单空格对照组:PDF/DOCX/XLSX/TXT,累计 16/16 → 200
公开附件 ZIP 包含:
- 10 个完全脱敏的 PDF、DOCX、XLSX、TXT 手工复现文件;
- 每种格式的单空格成功对照和双空格失败文件;
- 全角空格和不换行空格对照;
- SHA-256、脱敏上传结果及重复性 JSON;
- 使用说明。
ZIP SHA-256:DA62AB5CA4567A420B80C1845D82739B03DC233C57BE97E4711BE09E7C7695E1
附件内容为合成数据,不含用户、客户、公司、生产业务数据、本机路径或登录凭据。预签名 URL、签名、Cookie、Token、Request ID 和对象 key 均未写入附件或 Issue。
Workaround
上传前将文件名中的连续普通半角空格折叠为一个空格,或临时改为不含连续空格的文件名。该方式已经在相同文件字节上验证成功,但会改变用户原始文件名。
Description
聊天附件的文件名只要包含连续两个或以上普通半角空格(
U+0020),上传凭证接口可以正常返回 200,但随后向腾讯 COS 执行的预签名 PUT 会稳定返回403 SignatureDoesNotMatch。Web 最终只显示“上传失败”,用户容易误判为文件过大、格式不支持或网络异常。该问题已由两个真实文件独立复现:
两个样本均满足以下 A/B 结果:文件字节完全不变,只把连续空格折叠成一个空格后,PUT 从 403 变为 200;改为短文件名同样返回 200。因此本问题与文件内容、文件大小上限、文件格式、中文字符以及对象 key 是否使用原始文件名无关。
进一步使用完全脱敏的合成文件验证了 PDF、DOCX、XLSX、TXT 四种格式。每种格式的成功/失败文件在该格式内字节及 SHA-256 完全相同,仅文件名中的空格数量不同:
U+0020U+0020SignatureDoesNotMatchSignatureDoesNotMatchSignatureDoesNotMatchSignatureDoesNotMatch四个失败文件各执行 4 次新鲜凭证 + PUT,累计 16/16 均返回相同的 403;四个单空格对照累计 16/16 均返回 200。当前环境中的有界实测失败率为 100%。
字符边界矩阵另外覆盖了 25 类文件名。当前只确认连续两个及以上
U+0020会触发同一 COS 签名错误;单个半角空格、Tab、全角空格U+3000、不换行空格U+00A0、窄不换行空格U+202F、零宽空格U+200B、中文、Emoji 及常见标点均在当前链路 PUT 200。扩展名后的尾随空格会更早返回凭证 400,U+007F会更早被 HTTP 客户端拒绝,属于不同问题。当前代码和运行时证据指向 Server/COS 预签名边界:
modules/file/api.go的BuildContentDisposition会把原始 ASCII 空格保留在filename="..."中,同时生成filename*;modules/file/service_cos.go将该Content-Disposition加入签名 Header,并调用 MinIO GoPresignHeader;strings.Fields处理 Header 值;contentDisposition原样用于 PUT;Content-Length和相同文件字节直接执行 PUT,也会得到相同 403,排除了浏览器独有的 Header 改写;因此可以确认故障发生在 Server 生成并签名文件名相关 Header、再由 COS 验签的边界。COS 不返回其内部 canonical string,所以“双方具体在哪一步形成不同 canonical value”属于根据源码和 A/B 结果得到的机制推断;403、触发条件及故障边界本身已经实证。
历史记录:Mininglamp-OSS/octo-web#348 仍处于开放状态,并已在前端排查后标记为跨仓阻塞。该 Issue 早期评论认为后端未加引号或未生成 RFC 5987
filename*,但 2026-08-13 的线上响应已经同时包含这两项,因此旧判断不能解释当前复现。本 Issue 提供当前 Server 代码、真实 COS 返回和可公开夹具,作为后端独立跟踪项。Steps to Reproduce
01-PASS-Octo upload test.pdf。02-FAIL-Octo upload test.pdf;Octo与upload之间是两个普通半角空格。03/04(DOCX)、05/06(XLSX)、07/08(TXT)重复上述 A/B;每一对均只有一个空格与两个空格的区别。底层复现边界:对失败文件调用
/api/v1/file/upload/credentials会得到 200;随后携带响应中的Content-Type、Content-Disposition、准确Content-Length和文件字节执行 COS PUT,会得到 403SignatureDoesNotMatch。Expected Behavior
合法文件在大小和扩展名均允许的情况下,应能完成上传;文件名中连续的普通空格不应导致预签名 PUT 验证失败。字节相同的文件不应仅因文件名从一个空格变为两个空格而产生 200/403 的差异。
Actual Behavior
SignatureDoesNotMatch;该故障会阻断附件消息的后续发送,因此 Bot/OpenClaw 等下游也无法收到附件消息。失败后文件卡片可能继续残留或在后续会话同步时重新出现,这是 octo-web 的另一个失败队列生命周期现象,不是本 Server Issue 的 403 根因。
DMWork Version
线上环境,具体 DMWork 构建号未知;2026-08-13 仍稳定复现。相关用户曾提供 OpenClaw 包版本
2026.7.1-2,但 403 发生在附件上传阶段,与 OpenClaw 处理无关。Affected Component
Backend (Server)
Severity
P1 - Core Feature Broken
Reproducibility
Environment
https://im.deepminer.com.cn/Mininglamp-OSS/octo-servermain,2aee19bbf136d32e55411e02f79d7367d475ea37github.com/minio/minio-go/v7 v7.0.61Mininglamp-OSS/octo-webupstream/main,51bc8b3ee16209d226d3daad2947b14d137758f1Logs / Screenshots
失败链路的脱敏结果:
重复性结果:
公开附件 ZIP 包含:
ZIP SHA-256:
DA62AB5CA4567A420B80C1845D82739B03DC233C57BE97E4711BE09E7C7695E1附件内容为合成数据,不含用户、客户、公司、生产业务数据、本机路径或登录凭据。预签名 URL、签名、Cookie、Token、Request ID 和对象 key 均未写入附件或 Issue。
Workaround
上传前将文件名中的连续普通半角空格折叠为一个空格,或临时改为不含连续空格的文件名。该方式已经在相同文件字节上验证成功,但会改变用户原始文件名。