2026-04-30 02:30:32 +08:00

146 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.DisplayUserIDCooldown, xerr.DisplayUserIDPrettyNotAvailable, xerr.DisplayUserIDPrettyActive, xerr.DisplayUserIDLeaseExpired, xerr.DisplayUserIDPaymentRequired, xerr.CountryChangeCooldown, xerr.RegionCountryConflict, xerr.RegionDisabled:
return http.StatusConflict, string(reason), "conflict"
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())
}