From 2c45fa596adcb7124d8d8b737e38686965f8f2a4 Mon Sep 17 00:00:00 2001 From: soldosluka857 Date: Sat, 30 May 2026 02:08:16 +0000 Subject: [PATCH] fix: improve play profile input validation, error responses, and HTTP status codes - createPlayProfileHandler: return 201 Created (not 200) on success - create/update handlers: return 500 for infra errors, 400 only for validation errors (using new ErrPlayProfileValidation sentinel) - deletePlayProfileHandler: validate JSON body (was silently ignored) - verifyPlayProfilePINHandler: validate JSON body (was silently ignored) - verify handler catch-all: return 500 (not 400) for unexpected errors - Service layer: wrap validation errors with ErrPlayProfileValidation so handlers can distinguish client vs server errors Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- internal/handler/play_profile.go | 26 ++++++++++++++++++++------ internal/service/play_profile.go | 14 +++++++++----- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/internal/handler/play_profile.go b/internal/handler/play_profile.go index 788bc11..30ccf3d 100644 --- a/internal/handler/play_profile.go +++ b/internal/handler/play_profile.go @@ -51,11 +51,15 @@ func createPlayProfileHandler(svc *service.Container) gin.HandlerFunc { c.JSON(http.StatusBadRequest, gin.H{"error": "每个用户最多只能创建 3 个观影 Profile"}) return } - if err != nil { + if errors.Is(err, service.ErrPlayProfileValidation) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, row) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusCreated, row) } } @@ -76,10 +80,14 @@ func updatePlayProfileHandler(svc *service.Container) gin.HandlerFunc { c.JSON(http.StatusForbidden, gin.H{"error": "profile forbidden"}) return } - if err != nil { + if errors.Is(err, service.ErrPlayProfileValidation) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } c.JSON(http.StatusOK, row) } } @@ -89,7 +97,10 @@ func deletePlayProfileHandler(svc *service.Container) gin.HandlerFunc { uid, _ := c.Get(middleware.CtxUserID) userID := toString(uid) var req deletePlayProfileReq - _ = c.ShouldBindJSON(&req) + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) + return + } profile, err := svc.Repo.PlayProfile.FindByID(c.Request.Context(), c.Param("id")) if err != nil { @@ -141,7 +152,10 @@ func deletePlayProfileHandler(svc *service.Container) gin.HandlerFunc { func verifyPlayProfilePINHandler(svc *service.Container) gin.HandlerFunc { return func(c *gin.Context) { var req verifyPlayProfilePINReq - _ = c.ShouldBindJSON(&req) + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"}) + return + } uid, _ := c.Get(middleware.CtxUserID) profile, err := svc.PlayProfiles.VerifyPIN(c.Request.Context(), c.Param("id"), toString(uid), req.PIN) if errors.Is(err, service.ErrPlayProfileNotFound) { @@ -157,7 +171,7 @@ func verifyPlayProfilePINHandler(svc *service.Container) gin.HandlerFunc { return } if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } expiresAt := time.Now().Add(12 * time.Hour) diff --git a/internal/service/play_profile.go b/internal/service/play_profile.go index b6ae0fe..b32aed6 100644 --- a/internal/service/play_profile.go +++ b/internal/service/play_profile.go @@ -14,6 +14,7 @@ import ( "encoding/hex" "encoding/json" "errors" + "fmt" "strings" "time" @@ -36,6 +37,7 @@ var ( ErrPlayProfileForbidden = errors.New("profile forbidden") ErrPlayProfilePINInvalid = errors.New("pin invalid") ErrPlayProfileLimit = errors.New("profile limit reached") + ErrPlayProfileValidation = errors.New("validation error") ) // NewPlayProfileService is the constructor. @@ -194,7 +196,7 @@ func (s *PlayProfileService) updateExisting(ctx context.Context, row *model.Play if in.RequirePIN && in.PIN != "" { patch["pin_hash"] = hashPIN(in.PIN) } else if in.RequirePIN && row.PINHash == "" { - return nil, errors.New("pin required") + return nil, fmt.Errorf("%w: pin required", ErrPlayProfileValidation) } if !in.RequirePIN { patch["pin_hash"] = "" @@ -267,19 +269,21 @@ func (s *PlayProfileService) TouchActive(ctx context.Context, id string) error { // validateProfileInput rejects malformed payloads. On create we require // user_id; on update we allow it to be empty (caller supplies it via URL). +// Errors wrap ErrPlayProfileValidation so handlers can distinguish +// validation failures (400) from infrastructure errors (500). func validateProfileInput(in PlayProfileInput, requireUser bool) error { if strings.TrimSpace(in.Name) == "" { - return errors.New("name required") + return fmt.Errorf("%w: name required", ErrPlayProfileValidation) } if requireUser && strings.TrimSpace(in.UserID) == "" { - return errors.New("user_id required") + return fmt.Errorf("%w: user_id required", ErrPlayProfileValidation) } if requireUser && in.RequirePIN && strings.TrimSpace(in.PIN) == "" { - return errors.New("pin required") + return fmt.Errorf("%w: pin required", ErrPlayProfileValidation) } if in.RequirePIN && in.PIN != "" { if len(in.PIN) < 4 || len(in.PIN) > 8 { - return errors.New("pin must be 4-8 characters") + return fmt.Errorf("%w: pin must be 4-8 characters", ErrPlayProfileValidation) } } return nil