125 lines
3.9 KiB
Go
125 lines
3.9 KiB
Go
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"
|
||
codeAuthLoginBlocked = "AUTH_LOGIN_BLOCKED"
|
||
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) {
|
||
spec := xerr.SpecOf(reason)
|
||
return spec.HTTPStatus, spec.PublicCode, spec.PublicMessage
|
||
}
|
||
|
||
// 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())
|
||
}
|