---
date: 2026-05-11
type: error
tags: [express, send, sendFile, dotfiles, agent-pool, host-path]
---

# express `res.sendFile` 默认拒绝任何含 `/.xxx/` 段的绝对路径

## 现场

robot-manager attachment download 集成测试在 agent-pool slot 里跑稳定 500：

```
NotFoundError: Not Found
    at createHttpError (backend/node_modules/send/index.js:861:12)
    at SendStream.error (backend/node_modules/send/index.js:168:31)
    at SendStream.pipe (backend/node_modules/send/index.js:468:14)
    at sendfile (backend/node_modules/express/lib/response.js:1014:8)
    at ServerResponse.sendFile (backend/node_modules/express/lib/response.js:411:3)
    at RobotAttachmentController.download (.../robot-attachment.controller.ts:64:9)
```

service `getForDownload` 已经 `fs.existsSync(absPath)` 校验过文件存在（不存在会抛 404），
仍然 sendFile 报 NotFoundError——根因不在文件，而在路径里某一段。

## 根因

`express.sendFile(absPath)` 内部走 `send` 库。`send` 默认 `dotfiles: 'ignore'`，
检查路径任一**段**以 `.` 开头，命中即返回 404（用 "ignore" 当作 "not found"）。

```js
// send/index.js 类似逻辑
SendStream.prototype.containsDotFile = function (parts) {
  for (var i = 0; i < parts.length; i++) {
    var part = parts[i];
    if (part.length > 1 && part[0] === '.') return true;
  }
  return false;
};
```

注意是**路径任一段**——不只是 `path.basename`，整条 absPath 的每一段都查。

测试在 agent-pool slot 跑，绝对路径：

```
/home/chentao/Code/ffworkspace-wt/.agent-pool/slot-4/testing/uploads/robot-attachments/<uuid>/<file>
                                  ^^^^^^^^^^^
                                  这一段触发 dotfile 拒绝
```

CI runner 路径 `/home/runner/_work/.../uploads/...` 没有 dot 段——能过。
本地 dev `/home/chentao/Code/workspace/...` 也没——能过。
**只有 agent-pool slot 跑** 才命中。

## 为什么过去没人看到

issue #260 之前 jest `--runInBand` 在第 32 个 suite OOM 挂掉，robot-manager
按字母序在 organization 之后，**根本没跑到**。issue #260 batch by module
让全量真能跑完，加上这次 PR 在 agent-pool slot 内验证——dot 段命中暴露。

历史 PR 跑 robot-manager 都在 CI runner（非 slot）跑或者根本没跑到，
所以 dot-segment 这个坑潜伏到现在。

## 修

改用 `fs.createReadStream(absPath).pipe(res)`，绕过 send 库的整套安全策略：

```typescript
import * as fs from 'fs';

return new Promise<void>((resolve, reject) => {
  const stream = fs.createReadStream(absPath);
  stream.on('error', reject);
  stream.on('end', () => resolve());
  stream.pipe(res);
});
```

为什么不只加 `{ dotfiles: 'allow' }`：
- 治标——agent-pool 路径只是众多 dot 段宿主的一个；任何 host 把仓库放在
  `~/.config/<x>/repo/` `~/.local/share/<x>/` `~/.vscode/<x>/` 都中招
- stream pipe 一次性绕过 send 的**所有**路径安全检查（dotfile / null byte
  拒绝 / encoding round-trip 等）；attachment 的 absPath 由 service 算出，
  不接受外部输入，安全策略对它没意义
- 简单且鲁棒：stream + pipe 是 Node 文件下载的最小正确实现

## 验证

```
unset NODE_OPTIONS
bash testing/scripts/run-backend-integration.sh --force-reset backend/integration/robot-manager
Test Suites: 1 passed, 1 total
Tests:       128 passed, 128 total
```

## 适用范围

- 任何用 `res.sendFile` / `send` 库的项目，如果**宿主路径里有 dot 段**就中招
- 看 send 库默认拒绝清单（dotfiles=ignore）：CI 上常见的 `.git/`、`.cache/`、
  `.npm/`、`.next/`、agent 工具的 `.<tool-name>/` 子目录、Nix shell 的
  `.direnv/`、Python 的 `.venv/`——一旦仓库放在它们任何一个之下，sendFile 全失效
- 对内部计算路径的"白名单" sendFile 不安全：要么 `{ dotfiles: 'allow' }`，要么
  改 stream pipe；前者治标后者治根
- **集成测试要在多种宿主路径下跑过**：CI runner 路径 + dev 主机路径 +
  agent-pool slot 路径不一致，会让宿主路径相关的 bug 间歇性暴露

## 关联

- issue: #260（batch by module 让 robot-manager 真正跑过，浮出此 bug）
- 前置 ERR: ERR-20260511-001（最初定位为"非本 PR 引入"，根因调查在本 ERR）
- 受影响文件: backend/src/modules/robot-manager/robot-attachment.controller.ts
- send 库版本: backend/node_modules/send（依 express 5.x 间接依赖）
