package http import ( "context" "encoding/json" "io" "net/http" "strings" "hyapp/pkg/idgen" "hyapp/pkg/xerr" "google.golang.org/grpc/status" ) type requestIDContextKey struct{} const ( codeOK = "OK" codeInvalidJSON = "INVALID_JSON" codeInvalidArgument = "INVALID_ARGUMENT" codeUnauthorized = "UNAUTHORIZED" codePermissionDenied = "PERMISSION_DENIED" codeProfileRequired = "PROFILE_REQUIRED" codeNotFound = "NOT_FOUND" codeRateLimited = "RATE_LIMITED" codeUpstreamError = "UPSTREAM_ERROR" codeInternalError = "INTERNAL_ERROR" ) // responseEnvelope 是 gateway 暴露给客户端的稳定 JSON 外壳。 // HTTP status 表示传输层结果,code 表示客户端可依赖的错误语义。 type responseEnvelope struct { Code string `json:"code"` Message string `json:"message"` RequestID string `json:"request_id"` Data any `json:"data,omitempty"` } // withRequestID 为每个业务 API 请求建立追踪 ID。 // 客户端传入 X-Request-ID 时沿用,未传时由 gateway 生成,后续 gRPC RequestMeta 复用同一个值。 func withRequestID(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { requestID := strings.TrimSpace(request.Header.Get("X-Request-ID")) if requestID == "" { requestID = idgen.New("req") } writer.Header().Set("X-Request-ID", requestID) ctx := context.WithValue(request.Context(), requestIDContextKey{}, requestID) next.ServeHTTP(writer, request.WithContext(ctx)) }) } // requestIDFromContext 读取当前 HTTP 请求的追踪 ID。 // 理论上业务路由都会先经过 withRequestID;兜底生成值只用于防御错误调用。 func requestIDFromContext(ctx context.Context) string { requestID, _ := ctx.Value(requestIDContextKey{}).(string) if requestID != "" { return requestID } return idgen.New("req") } // decodeJSON 隔离 JSON 解码细节,避免 handler 直接依赖 envelope 写法。 func decodeJSON(reader io.Reader, out any) error { return json.NewDecoder(reader).Decode(out) } // writeOK 写成功 envelope。 func writeOK(writer http.ResponseWriter, request *http.Request, data any) { writeEnvelope(writer, http.StatusOK, responseEnvelope{ Code: codeOK, Message: "ok", RequestID: responseRequestID(request), Data: data, }) } // writeError 写失败 envelope。 // 错误 message 保持稳定、短小,排障依赖 request_id 追日志,而不是把内部错误直接暴露给客户端。 func writeError(writer http.ResponseWriter, request *http.Request, statusCode int, code string, message string) { writeEnvelope(writer, statusCode, responseEnvelope{ Code: code, Message: message, RequestID: responseRequestID(request), }) } // writeRPCError 把内部 gRPC status + ErrorInfo.reason 转成外部稳定 envelope。 func writeRPCError(writer http.ResponseWriter, request *http.Request, err error) { if err == nil { return } if _, ok := status.FromError(err); !ok { writeError(writer, request, http.StatusBadGateway, codeUpstreamError, "upstream service error") return } reason := xerr.ReasonFromGRPC(err) statusCode, code, message := mapReasonToHTTP(reason) writeError(writer, request, statusCode, code, message) } func mapReasonToHTTP(reason xerr.Code) (int, string, string) { switch reason { case xerr.InvalidArgument, xerr.DisplayUserIDInvalid: return http.StatusBadRequest, string(reason), "invalid argument" case xerr.AuthFailed: return http.StatusUnauthorized, string(reason), "authentication failed" case xerr.Unauthorized, xerr.SessionExpired, xerr.SessionRevoked: return http.StatusUnauthorized, string(reason), "unauthorized" case xerr.Conflict, xerr.PasswordAlreadySet, xerr.DisplayUserIDExists: return http.StatusConflict, string(reason), "conflict" case xerr.RoomClosed: return http.StatusConflict, string(reason), "room closed" case xerr.DisplayUserIDCooldown, xerr.DisplayUserIDPrettyNotAvailable, xerr.DisplayUserIDPrettyActive, xerr.DisplayUserIDLeaseExpired, xerr.DisplayUserIDPaymentRequired, xerr.CountryChangeCooldown, xerr.RegionCountryConflict, xerr.RegionDisabled: return http.StatusConflict, string(reason), "conflict" case xerr.InsufficientBalance: return http.StatusConflict, string(reason), "insufficient balance" case xerr.ProfileRequired: return http.StatusForbidden, codeProfileRequired, "profile required" case xerr.UserDisabled, xerr.PermissionDenied: return http.StatusForbidden, string(reason), "permission denied" case xerr.NotFound, xerr.DisplayUserIDNotFound, xerr.CountryNotFound, xerr.RegionNotFound: return http.StatusNotFound, string(reason), "not found" case xerr.Unavailable: return http.StatusBadGateway, codeUpstreamError, "upstream service error" case xerr.Internal: return http.StatusInternalServerError, codeInternalError, "internal error" default: return http.StatusBadGateway, codeUpstreamError, "upstream service error" } } // writeEnvelope 是 gateway 业务 API 的唯一 JSON 响应出口。 func writeEnvelope(writer http.ResponseWriter, statusCode int, response responseEnvelope) { writer.Header().Set("Content-Type", "application/json") writer.WriteHeader(statusCode) if err := json.NewEncoder(writer).Encode(response); err != nil { return } } // responseRequestID 统一确定响应中的 request_id。 func responseRequestID(request *http.Request) string { return requestIDFromContext(request.Context()) }