fix: 5xx 的 detail 留在响应里,改为补日志侧留痕(自我纠正)

前一个提交按 §4.2 的旧判据「internal(err) 把原始错误串写进响应 detail(应仅日志)」
把 5xx 的 detail 去掉了。这是改错了方向,本提交翻回来:

- 管理端 describeError()(systemApi.ts:85)是故意把 error.detail 拼进提示的,
  EventInfoPanel / UsersPanel / FontUploadField / ImageUploadField 等 5 处都在用。
  录人员常常在局域网平板上操作,够不到服务端控制台的日志面板——去掉 detail
  等于把失败原因藏在他拿不到的地方。
- 旧判据还漏了:原来真正的缺口不是「响应里有原文」,而是「日志里没有原文」。
  正确修法是补日志,不是砍响应。
- 现口径:两边都要。apiError.detail 与 apiError.logDetail 是两个独立字段
  (一个管回不回前端、一个管日志记什么),5xx 目前两者都设;要收口时只把
  detail 置 nil 即可,日志线索不受影响。
- 测试 TestServerErrorKeepsRawReasonOutOfResponse 改名并反转断言为
  TestServerErrorCarriesRawReasonToClientAndLog:同时断言响应 detail 带原文、
  ERROR 日志也带原文。

版本号 dev_0.1.3 → dev_0.1.4(0.1.2 / 0.1.3 均未交付,debug/ 只留 0.1.4)。
This commit is contained in:
AskaEth committed 2026-09-11 14:46:10 +08:00
1 parent b4bdaef9ba
commit a52e6a4a3b
3 files changed
+23 -15

No files matched your search

+14 -4
View File
@@ -16,9 +16,10 @@ type apiError struct {
code string
message string
detail any
// logDetail 是「只写日志、不进响应体」的错误原文,只有 5xx 用:
// SQLite / 计分引擎的原始错误串里可能带表名列名甚至数据片段,
// 现场排查需要它,前端不需要、也不该拿到。
// logDetail 是给日志用的错误原文(5xx)。它和 detail 是**两件独立的事**:
// detail 控制「回不回前端」,logDetail 控制「日志里记什么」,5xx 目前两者都设。
// 分开是为了留住选择权——哪天要收口响应体,把 detail 设成 nil 即可,
// 日志侧的排查线索不会跟着一起丢。
logDetail string
// origin 是「拦截点」:构造这个错误的 file:line 与函数名,例如
// internal/httpapi/repo_events.go:231 normalizeAvgMode。拦截日志靠它定位到校验代码。
@@ -100,16 +101,25 @@ func wrongCredential(message string) *apiError {
return newAPIError(http.StatusBadRequest, "AUTH_FAILED", message, nil)
}
// internal 是 500:给现场看的只有 message,原始错误串走 logDetail 只进日志。
// internal 是 500:message 是给人看的笼统文案,原始错误串由 internalMsg 同时回前端与写日志。
func internal(err error) *apiError {
return internalMsg("服务端异常", err)
}
// internalMsg 是 internal 的可自定义文案版本:5xx 的 message 也要让现场看懂发生了什么。
// skip=4:业务代码 → internal/internalMsg → newAPIErrorSkip → callerOrigin,比 newAPIError 多一层。
//
// 原始错误串**同时**回给前端(detail)并写进日志(logDetail),两边都要:
// - 前端:管理端 `describeError()` 会把 detail 拼在提示里,录人员常常在局域网平板上
// 操作,看不到服务端控制台的日志面板,只给一句「服务端异常」等于把原因藏在他够不着的地方;
// - 日志:此前 5xx 只把原文回了响应、日志里只有笼统 message,服务端自己反而没留痕。
//
// 本系统完全离线内网、账号可控,用「表名列名可见」换现场可诊断性是划算的;
// 真正要防的是请求体里的证件号,那部分仍然不记(见 origin.go 的说明)。
func internalMsg(message string, err error) *apiError {
api := newAPIErrorSkip(4, http.StatusInternalServerError, "INTERNAL", message, nil)
if err != nil {
api.detail = err.Error()
api.logDetail = err.Error()
}
return api