Files

153 lines
4.6 KiB
Go

package response
import (
"errors"
"net/http"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/util/gvalid"
"github.com/qianfree/team-api/internal/consts"
)
// jsonResp is the unified response structure for all API endpoints.
type jsonResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
RequestID string `json:"request_id"`
}
// Success writes a successful response: HTTP 200 + {"code":0, "message":"ok", "data":..., "request_id":...}.
func Success(r *ghttp.Request, data interface{}) {
requestID := getRequestID(r)
r.Response.WriteJson(jsonResp{
Code: 0,
Message: consts.MsgSuccess,
Data: data,
RequestID: requestID,
})
r.SetError(nil) // 清除请求上的错误,防止 ErrorHandler 中间件重复处理
}
// Error writes an error response with automatic sanitization.
//
// Security: Only business errors (code >= 10000), validation errors, and standard HTTP 4xx errors
// expose their messages to the client. All other errors (database, network,
// internal) are replaced with "服务器内部错误" to prevent information leakage.
// The original error is logged with request_id for debugging.
func Error(r *ghttp.Request, err error) {
code := 500
msg := "服务器内部错误"
// Check for GoFrame validation error first (gvalid.Error is NOT a gerror.Error,
// so the gerror branch below won't match it — must check separately)
var validErr gvalid.Error
if errors.As(err, &validErr) {
code = 422
msg = validErr.Current().Error()
} else {
// Extract code from GoFrame gerror (supports wrapped errors via errors.As)
var gerr *gerror.Error
if errors.As(err, &gerr) {
code = gerr.Code().Code()
// Business errors (>= 10000) and HTTP 4xx have safe user-facing messages
if code >= 10000 || (code >= 400 && code < 500) {
msg = gerr.Error()
} else {
// Internal error — sanitize message, log real cause
g.Log().Warning(r.Context(), "[Response] Internal error sanitized",
"request_id", getRequestID(r),
"code", code,
"error", err.Error(),
)
// Persist system error to sys_error_logs
writeErrorLog(&ErrorLogRecord{
RequestId: getRequestID(r),
ErrorCode: code,
ErrorMessage: err.Error(),
StackTrace: captureStackTrace(3),
HttpMethod: r.Method,
RequestPath: r.URL.Path,
RequestBody: safeRequestBodyForLog(r),
Source: "api",
})
}
} else {
// Non-gerror (raw Go error, e.g. database driver error) — always sanitize
g.Log().Warning(r.Context(), "[Response] Raw error sanitized",
"request_id", getRequestID(r),
"error", err.Error(),
)
// Persist raw error to sys_error_logs
writeErrorLog(&ErrorLogRecord{
RequestId: getRequestID(r),
ErrorCode: 500,
ErrorMessage: err.Error(),
StackTrace: captureStackTrace(3),
HttpMethod: r.Method,
RequestPath: r.URL.Path,
RequestBody: safeRequestBodyForLog(r),
Source: "api",
})
}
}
if code == 0 {
code = 500
}
// Determine HTTP status
httpStatus := code
if code >= 10000 {
httpStatus = 422
} else if httpStatus < 100 || httpStatus > 599 {
// GoFrame gcode 可能返回非标准错误码(如 68=CodeNotModified),
// 不能直接用作 HTTP 状态码,否则 WriteHeader 会 panic
httpStatus = 500
}
requestID := getRequestID(r)
r.Response.WriteHeader(httpStatus)
r.Response.WriteJson(jsonResp{
Code: code,
Message: msg,
Data: nil,
RequestID: requestID,
})
r.SetError(nil) // 清除请求上的错误,防止 ErrorHandler 中间件重复处理
}
// Use this for 400/401/403/404/409/429/500 errors.
func ErrorMsg(r *ghttp.Request, code int, message string) {
ErrorWithCode(r, code, code, message)
}
// ErrorWithCode writes an error with separate HTTP status and business code.
// Use this when HTTP status differs from the business code (e.g., HTTP 401 with code 10020).
func ErrorWithCode(r *ghttp.Request, httpStatus int, code int, message string) {
if httpStatus < 100 || httpStatus > 599 {
httpStatus = http.StatusInternalServerError
}
requestID := getRequestID(r)
r.Response.WriteHeader(httpStatus)
r.Response.WriteJson(jsonResp{
Code: code,
Message: message,
Data: nil,
RequestID: requestID,
})
r.SetError(nil) // 清除请求上的错误,防止 ErrorHandler 中间件重复处理
}
// getRequestID extracts the request ID from context.
func getRequestID(r *ghttp.Request) string {
if v := r.GetCtxVar("RequestId"); v != nil {
return v.String()
}
return ""
}