Admin 上传失败:全局拦截器覆盖 Content-Type 导致 Excel 导入不可用 #139

Closed
opened 2026-08-28 21:06:20 +08:00 by ila · 3 comments
Owner

所属与来源

  • 关联工单:#117~#122 档口入库码 MVP(本缺陷发生在其导入功能上)。
  • 来源:用户于 2026-08-28 反馈:Admin 档口入库码模块导入 Excel 失败,提示「没有收到 10MB 以内的 Excel」。
  • 类型:Admin 前端 + Server / 文件上传请求头缺陷与错误提示改进。
  • 设计证据:不新增或调整页面、组件、导航;仅修复上传失败与调整一处错误提示文案,属恢复既有预期行为的缺陷修复,不需要原型。
  • 工具回退说明:本工单通过 Gitea API 创建;当前会话未提供 Gitea MCP 工具,按 AGENTS.md「Gitea 交互与工单最小读取」记录回退原因。

根因(提交 30d8238 复核)

与文件大小无关,任意大小的 Excel 都会失败。

主因:全局请求拦截器无条件覆盖 Content-Type

web/src/utils/request.js:18-24:

if (store.getters.token) {
  config.headers['Authorization'] = 'Bearer ' + getToken()
  config.headers['Content-Type'] = 'application/json'   // 无条件覆盖
}

只要用户已登录(上传场景必然成立),所有请求的 Content-Type 都被强制改为 application/json,包括本次的 multipart 上传。

于是请求体是 multipart 格式、请求头却声明为 JSON。服务端 server/app/goauto/sybinnercode/handler.go:54 的 c.FormFile("file") 无法解析 multipart 而返回错误,进入 :56 输出「没有收到 10MB 以内的 Excel」。

次因:API 层手写 Content-Type 丢失 boundary

web/src/api/goauto/syb-inner-codes.js:6:

headers: { 'Content-Type': 'multipart/form-data' }

手写该值会丢掉 boundary。正确的头部形如 multipart/form-data; boundary=----WebKitFormBoundaryXXX,boundary 由浏览器在序列化 FormData 时生成。即使修复了主因,只要这行仍在,服务端依旧无法解析。

两处必须同时修,只修其一仍然失败。

附带问题:错误提示掩盖真实原因

handler.go:56 把「表单字段名不匹配」「Content-Type 不正确」「请求体超过上限」等不同失败统一输出为「没有收到 10MB 以内的 Excel」,导致排查方向被引向文件体积,而真实原因是请求头。

服务端的大小上限本身是正确的:MaxUploadBytes = 10 * 1024 * 1024(types.go:11)、MaxBytesReader 留了 1MB 表单开销(handler.go:53)、parser.go:36-37 另有独立的超限提示。

目标

  1. 档口入库码 Excel 导入恢复可用。
  2. 修复在全局拦截器层面完成,避免其他上传功能重复踩同一问题。
  3. 上传失败的错误提示能区分「请求格式无效」与「文件超过上限」。

非目标

  • 不修改上传的大小上限与解析逻辑。
  • 不修改导入的业务处理、匹配与回写流程。
  • 不改动页面布局、组件与既有交互。
  • 不重构 request.js 的其他拦截逻辑(鉴权、响应处理、错误提示保持原样)。

实施方案

前端

  1. web/src/utils/request.js:仅在请求体不是 FormData 时才设置 JSON 的 Content-Type;Authorization 的设置逻辑保持不变。

    config.headers['Authorization'] = 'Bearer ' + getToken()
    if (!(config.data instanceof FormData)) {
      config.headers['Content-Type'] = 'application/json'
    }
    
  2. web/src/api/goauto/syb-inner-codes.js:6:删除手写的 headers: { 'Content-Type': 'multipart/form-data' },交由浏览器自动生成带 boundary 的头部。

  3. 全局检查:确认项目中其他以 FormData 提交的接口没有同类手写 Content-Type 的写法;若有一并修正,并在工单列出清单。

服务端

  1. server/app/goauto/sybinnercode/handler.go:54-57:区分失败原因——
    • 请求体超过 MaxBytesReader 上限时提示「Excel 超过 10MB 上限」,与 parser.go:37 的现有文案保持一致;
    • 其余解析失败(字段名不匹配、Content-Type 不正确、表单格式错误)提示「上传格式无效,请重新选择 Excel 后重试」。
  2. 不改变任何校验强度:ValidateUpload 的文件名、大小与文件头校验保持原样。

安全边界

  • 上传大小上限与文件头校验不放宽。
  • 鉴权逻辑不变,Authorization 头在所有请求上照常携带。
  • 错误提示中不得包含文件路径、用户信息与服务端内部错误细节。

验收标准

  • 档口入库码模块可成功导入正常大小的 .xlsx 文件。
  • 已登录状态下上传请求的 Content-Type 为浏览器生成的 multipart/form-data; boundary=...,未被覆盖为 application/json。
  • 非 FormData 的普通接口仍以 application/json 发送,既有功能无回归。
  • 超过 10MB 的文件被拒绝,提示为「Excel 超过 10MB 上限」。
  • 构造 Content-Type 或字段名错误的请求时,提示为「上传格式无效」,不再误报为大小问题。
  • 项目中其他 FormData 提交路径已排查,结果在工单说明。
  • 上传大小上限与文件头校验强度未被削弱。

验证方式

  • 前端构建与既有 Web 测试通过。
  • go test ./app/goauto/sybinnercode/...
  • 手工验证四条路径:正常文件导入成功、超过 10MB 被拒且提示正确、字段名错误提示正确、普通 JSON 接口无回归。
  • 未覆盖的浏览器与异常路径如实回写。

依赖、并行与风险

  • 无前置依赖,可立即实施;与其他在途工单无文件重叠。
  • 风险:request.js 是全局拦截器,改动影响所有请求。缓解:改动仅为增加一个 FormData 判断条件,不触碰其他分支;验收含普通接口的回归检查。
  • 回退:还原提交即可,无数据影响。

文档影响

  • 预计无长期文档影响:不改变模块入口、接口契约、配置与数据结构,仅修复请求头与调整一处错误提示文案。实施完成后在工单记录该结论并跳过 Wiki 更新与同步;若排查中发现需要在 Troubleshooting 记录该类上传失败的定位方法,再按 Wiki-first 门禁更新。

状态

待验收(实现提交 9d23b44,已推送至 origin/main)。

## 所属与来源 - 关联工单:#117~#122 档口入库码 MVP(本缺陷发生在其导入功能上)。 - 来源:用户于 2026-08-28 反馈:Admin 档口入库码模块导入 Excel 失败,提示「没有收到 10MB 以内的 Excel」。 - 类型:Admin 前端 + Server / 文件上传请求头缺陷与错误提示改进。 - 设计证据:不新增或调整页面、组件、导航;仅修复上传失败与调整一处错误提示文案,属恢复既有预期行为的缺陷修复,不需要原型。 - 工具回退说明:本工单通过 Gitea API 创建;当前会话未提供 Gitea MCP 工具,按 `AGENTS.md`「Gitea 交互与工单最小读取」记录回退原因。 ## 根因(提交 30d8238 复核) **与文件大小无关**,任意大小的 Excel 都会失败。 ### 主因:全局请求拦截器无条件覆盖 Content-Type `web/src/utils/request.js:18-24`: ```js if (store.getters.token) { config.headers['Authorization'] = 'Bearer ' + getToken() config.headers['Content-Type'] = 'application/json' // 无条件覆盖 } ``` 只要用户已登录(上传场景必然成立),**所有请求的 `Content-Type` 都被强制改为 `application/json`**,包括本次的 multipart 上传。 于是请求体是 multipart 格式、请求头却声明为 JSON。服务端 `server/app/goauto/sybinnercode/handler.go:54` 的 `c.FormFile("file")` 无法解析 multipart 而返回错误,进入 `:56` 输出「没有收到 10MB 以内的 Excel」。 ### 次因:API 层手写 Content-Type 丢失 boundary `web/src/api/goauto/syb-inner-codes.js:6`: ```js headers: { 'Content-Type': 'multipart/form-data' } ``` 手写该值会**丢掉 boundary**。正确的头部形如 `multipart/form-data; boundary=----WebKitFormBoundaryXXX`,boundary 由浏览器在序列化 FormData 时生成。即使修复了主因,只要这行仍在,服务端依旧无法解析。 **两处必须同时修**,只修其一仍然失败。 ### 附带问题:错误提示掩盖真实原因 `handler.go:56` 把「表单字段名不匹配」「Content-Type 不正确」「请求体超过上限」等不同失败统一输出为「没有收到 10MB 以内的 Excel」,导致排查方向被引向文件体积,而真实原因是请求头。 服务端的大小上限本身是正确的:`MaxUploadBytes = 10 * 1024 * 1024`(`types.go:11`)、`MaxBytesReader` 留了 1MB 表单开销(`handler.go:53`)、`parser.go:36-37` 另有独立的超限提示。 ## 目标 1. 档口入库码 Excel 导入恢复可用。 2. 修复在全局拦截器层面完成,避免其他上传功能重复踩同一问题。 3. 上传失败的错误提示能区分「请求格式无效」与「文件超过上限」。 ## 非目标 - 不修改上传的大小上限与解析逻辑。 - 不修改导入的业务处理、匹配与回写流程。 - 不改动页面布局、组件与既有交互。 - 不重构 `request.js` 的其他拦截逻辑(鉴权、响应处理、错误提示保持原样)。 ## 实施方案 ### 前端 1. `web/src/utils/request.js`:仅在请求体不是 `FormData` 时才设置 JSON 的 `Content-Type`;`Authorization` 的设置逻辑保持不变。 ```js config.headers['Authorization'] = 'Bearer ' + getToken() if (!(config.data instanceof FormData)) { config.headers['Content-Type'] = 'application/json' } ``` 2. `web/src/api/goauto/syb-inner-codes.js:6`:删除手写的 `headers: { 'Content-Type': 'multipart/form-data' }`,交由浏览器自动生成带 boundary 的头部。 3. 全局检查:确认项目中其他以 `FormData` 提交的接口没有同类手写 Content-Type 的写法;若有一并修正,并在工单列出清单。 ### 服务端 4. `server/app/goauto/sybinnercode/handler.go:54-57`:区分失败原因—— - 请求体超过 `MaxBytesReader` 上限时提示「Excel 超过 10MB 上限」,与 `parser.go:37` 的现有文案保持一致; - 其余解析失败(字段名不匹配、Content-Type 不正确、表单格式错误)提示「上传格式无效,请重新选择 Excel 后重试」。 5. 不改变任何校验强度:`ValidateUpload` 的文件名、大小与文件头校验保持原样。 ## 安全边界 - 上传大小上限与文件头校验不放宽。 - 鉴权逻辑不变,`Authorization` 头在所有请求上照常携带。 - 错误提示中不得包含文件路径、用户信息与服务端内部错误细节。 ## 验收标准 - [ ] 档口入库码模块可成功导入正常大小的 `.xlsx` 文件。 - [ ] 已登录状态下上传请求的 `Content-Type` 为浏览器生成的 `multipart/form-data; boundary=...`,未被覆盖为 `application/json`。 - [ ] 非 FormData 的普通接口仍以 `application/json` 发送,既有功能无回归。 - [ ] 超过 10MB 的文件被拒绝,提示为「Excel 超过 10MB 上限」。 - [ ] 构造 Content-Type 或字段名错误的请求时,提示为「上传格式无效」,不再误报为大小问题。 - [ ] 项目中其他 FormData 提交路径已排查,结果在工单说明。 - [ ] 上传大小上限与文件头校验强度未被削弱。 ## 验证方式 - 前端构建与既有 Web 测试通过。 - `go test ./app/goauto/sybinnercode/...` - 手工验证四条路径:正常文件导入成功、超过 10MB 被拒且提示正确、字段名错误提示正确、普通 JSON 接口无回归。 - 未覆盖的浏览器与异常路径如实回写。 ## 依赖、并行与风险 - 无前置依赖,可立即实施;与其他在途工单无文件重叠。 - 风险:`request.js` 是全局拦截器,改动影响所有请求。缓解:改动仅为增加一个 `FormData` 判断条件,不触碰其他分支;验收含普通接口的回归检查。 - 回退:还原提交即可,无数据影响。 ## 文档影响 - 预计无长期文档影响:不改变模块入口、接口契约、配置与数据结构,仅修复请求头与调整一处错误提示文案。实施完成后在工单记录该结论并跳过 Wiki 更新与同步;若排查中发现需要在 `Troubleshooting` 记录该类上传失败的定位方法,再按 Wiki-first 门禁更新。 ## 状态 待验收(实现提交 `9d23b44`,已推送至 `origin/main`)。
Author
Owner

开始实施

已核对 #139 最新正文、当前 main@6f611a0 与工作区;仅有无关未跟踪项 gitea.zip、prototypes/130/,将保持不动。

全局排查确认共有三条真实 FormData 路径:档口入库码导入、用户头像上传、通用 ImageCropper。后两条未手写 multipart 头,但同样会被当前全局拦截器覆盖,因此在拦截器统一修复后一起恢复;仅档口入库码 API 还需删除手写 multipart/form-data。本单无数据库、权限、状态机和长期文档影响,按已确认方案进入实施。

## 开始实施 已核对 #139 最新正文、当前 `main@6f611a0` 与工作区;仅有无关未跟踪项 `gitea.zip`、`prototypes/130/`,将保持不动。 全局排查确认共有三条真实 FormData 路径:档口入库码导入、用户头像上传、通用 ImageCropper。后两条未手写 multipart 头,但同样会被当前全局拦截器覆盖,因此在拦截器统一修复后一起恢复;仅档口入库码 API 还需删除手写 `multipart/form-data`。本单无数据库、权限、状态机和长期文档影响,按已确认方案进入实施。
Author
Owner

实施完成,待验收

已按工单范围完成并推送:9d23b44 fix(upload): preserve multipart boundaries (#139)。

实现结果

  • 全局请求拦截器现在仅对非 FormData 请求设置 application/json;鉴权头逻辑保持不变。
  • 档口入库码导入删除手写 multipart/form-data,由浏览器生成带 boundary 的请求头。
  • 服务端将上传体超限与 multipart/字段格式错误分开提示;10MB 限制和 xlsx 文件头校验保持不变。
  • 全局排查共 3 条 FormData 路径:
    1. web/src/api/goauto/syb-inner-codes.js
    2. web/src/views/profile/userAvatar.vue
    3. web/src/components/ImageCropper/index.vue
      仅第一条存在手写 multipart 头;后两条未手写,随全局拦截器修复一并恢复。

自动化验证

  • go test ./app/goauto/sybinnercode/...:通过。
  • 浏览器式 multipart boundary、正常 xlsx、错误 Content-Type、错误字段名、请求体超限、10MB 业务上限:新增服务端测试并通过。
  • FormData 不写 JSON 头、普通对象仍写 JSON 头:新增 Web 单测并通过。
  • pnpm test:unit:8 个套件、31 个测试全部通过。
  • pnpm build:prod:通过。
  • python dev_scripts/harness.py check --strict:通过。
  • .\scripts\verify.ps1 -Component all:通过(Server 全量测试/构建、Web lint/构建、Android 单测与 debug APK 构建)。Web lint、CSS 与包体积仅输出既有警告,无错误。
  • git diff --check:通过。

首次定向 Jest 执行曾因 Windows 临时缓存文件缺失失败;清理 Jest cache 后重跑通过,全量单测随后也通过。该失败与产品代码无关。

未验证与文档影响

  • 未在真实登录浏览器中手工上传文件;待人工验收正常 xlsx、超过 10MB 和普通 JSON 请求路径。
  • 无长期文档影响:接口、数据结构、配置、入口和业务规则均未变化,按工单约定跳过 Wiki 更新与同步。
  • 无数据库迁移、权限、状态机或数据变更。

工单保持打开,等待人工验收。

## 实施完成,待验收 已按工单范围完成并推送:`9d23b44 fix(upload): preserve multipart boundaries (#139)`。 ### 实现结果 - 全局请求拦截器现在仅对非 `FormData` 请求设置 `application/json`;鉴权头逻辑保持不变。 - 档口入库码导入删除手写 `multipart/form-data`,由浏览器生成带 `boundary` 的请求头。 - 服务端将上传体超限与 multipart/字段格式错误分开提示;10MB 限制和 xlsx 文件头校验保持不变。 - 全局排查共 3 条 FormData 路径: 1. `web/src/api/goauto/syb-inner-codes.js` 2. `web/src/views/profile/userAvatar.vue` 3. `web/src/components/ImageCropper/index.vue` 仅第一条存在手写 multipart 头;后两条未手写,随全局拦截器修复一并恢复。 ### 自动化验证 - `go test ./app/goauto/sybinnercode/...`:通过。 - 浏览器式 multipart boundary、正常 xlsx、错误 Content-Type、错误字段名、请求体超限、10MB 业务上限:新增服务端测试并通过。 - FormData 不写 JSON 头、普通对象仍写 JSON 头:新增 Web 单测并通过。 - `pnpm test:unit`:8 个套件、31 个测试全部通过。 - `pnpm build:prod`:通过。 - `python dev_scripts/harness.py check --strict`:通过。 - `.\scripts\verify.ps1 -Component all`:通过(Server 全量测试/构建、Web lint/构建、Android 单测与 debug APK 构建)。Web lint、CSS 与包体积仅输出既有警告,无错误。 - `git diff --check`:通过。 首次定向 Jest 执行曾因 Windows 临时缓存文件缺失失败;清理 Jest cache 后重跑通过,全量单测随后也通过。该失败与产品代码无关。 ### 未验证与文档影响 - 未在真实登录浏览器中手工上传文件;待人工验收正常 xlsx、超过 10MB 和普通 JSON 请求路径。 - 无长期文档影响:接口、数据结构、配置、入口和业务规则均未变化,按工单约定跳过 Wiki 更新与同步。 - 无数据库迁移、权限、状态机或数据变更。 工单保持打开,等待人工验收。
Author
Owner

用户于 2026-08-29 明确确认本工单通过验收。验收结论已记录,现关闭工单。没有新的长期事实变化,本次不重复同步 Wiki。

用户于 2026-08-29 明确确认本工单通过验收。验收结论已记录,现关闭工单。没有新的长期事实变化,本次不重复同步 Wiki。
ila closed this issue 2026-08-29 20:44:00 +08:00
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: OPC/goauto#139