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:
1 parent
b4bdaef9ba
commit
a52e6a4a3b
3 files changed
+23
-15
No files matched your search
@@ -12,10 +12,11 @@ import (
|
||||
"git.yeij.top/AskaEth/Scoreboard/server/internal/httpapi"
|
||||
)
|
||||
|
||||
// 5xx 的原始错误串(SQLite / 计分引擎的报错)只能进日志,不能进响应体:
|
||||
// 里面有表名、列名甚至数据片段,前端不需要、也不该拿到。
|
||||
// 排查能力不能因此丢——ERROR 行必须带上那句原文。
|
||||
func TestServerErrorKeepsRawReasonOutOfResponse(t *testing.T) {
|
||||
// 5xx 的原始错误串要**同时**到两处,缺一个现场都会变瞎:
|
||||
// - 响应体 detail —— 管理端 `describeError()` 把它拼在提示上,录人员常在局域网平板上
|
||||
// 操作,够不到服务端控制台的日志面板;
|
||||
// - ERROR 日志 —— 此前只有响应里有原文、日志里只有笼统 message,服务端自己没留痕。
|
||||
func TestServerErrorCarriesRawReasonToClientAndLog(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
logger := slog.New(slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
env := newTestEnvWith(t, func(o *httpapi.Options) {
|
||||
@@ -34,9 +35,6 @@ func TestServerErrorKeepsRawReasonOutOfResponse(t *testing.T) {
|
||||
t.Fatalf("未知大屏指令应 500,得到 %d %s", status, body)
|
||||
}
|
||||
|
||||
if strings.Contains(string(body), "未知的大屏指令") {
|
||||
t.Fatalf("响应体不该回显内部错误原文,得到 %s", body)
|
||||
}
|
||||
var payload struct {
|
||||
OK bool `json:"ok"`
|
||||
Error struct {
|
||||
@@ -49,10 +47,10 @@ func TestServerErrorKeepsRawReasonOutOfResponse(t *testing.T) {
|
||||
t.Fatalf("解析响应体: %v (%s)", err, body)
|
||||
}
|
||||
if payload.Error.Code != "INTERNAL" || payload.Error.Message != "服务端异常" {
|
||||
t.Fatalf("5xx 应回笼统文案,得到 %+v", payload.Error)
|
||||
t.Fatalf("5xx 的 code 与笼统文案要保持,得到 %+v", payload.Error)
|
||||
}
|
||||
if len(payload.Error.Detail) != 0 {
|
||||
t.Fatalf("5xx 的 detail 应为空,得到 %s", payload.Error.Detail)
|
||||
if !strings.Contains(string(payload.Error.Detail), "未知的大屏指令") {
|
||||
t.Fatalf("响应体 detail 应带上原始错误串(现场要能直接看懂为什么失败),得到 %s", payload.Error.Detail)
|
||||
}
|
||||
|
||||
// 原文必须能在日志里找到,否则现场就真的查不下去了。
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user