Files
a301_server/pkg/apperror/apperror.go
tolelom b006fe77c2
All checks were successful
Server CI/CD / lint-and-build (push) Successful in 38s
Server CI/CD / deploy (push) Successful in 50s
fix: API 서버 코드 리뷰 버그 15건 수정 (CRITICAL 2, HIGH 2, MEDIUM 11)
CRITICAL:
- graceful shutdown 레이스 수정 — Listen을 goroutine으로 이동
- Register 레이스 컨디션 — sentinel error + MySQL duplicate key 처리

HIGH:
- 멱등성 키에 method+path 포함 — 엔드포인트 간 캐시 충돌 방지
- 입장 토큰 생성 실패 시 방/슬롯 롤백 추가

MEDIUM:
- RequestEntry 슬롯 없음 시 503 반환
- chain ExportWallet/GetWalletInfo/GrantReward 에러 처리 개선
- resolveUsername 에러 타입 구분 (duplicate key vs 기타)
- 공지사항 길이 검증 byte→rune (한국어 256자 허용)
- Level 검증 범위 MaxLevel(50)로 통일
- admin 자기 강등 방지
- CORS ExposeHeaders 추가
- MySQL DSN loc=Local→loc=UTC
- hashGameExeFromZip 100MB 초과 절단 감지

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 18:05:27 +09:00

78 lines
2.8 KiB
Go

package apperror
import (
"errors"
"fmt"
"strings"
"github.com/go-sql-driver/mysql"
)
// AppError is a structured application error with an HTTP status code.
// JSON response format: {"error": "<code>", "message": "<human-readable message>"}
type AppError struct {
Code string `json:"error"`
Message string `json:"message"`
Status int `json:"-"`
}
func (e *AppError) Error() string { return e.Message }
// New creates a new AppError.
func New(code string, message string, status int) *AppError {
return &AppError{Code: code, Message: message, Status: status}
}
// Wrap creates a new AppError that wraps a cause error.
func Wrap(code string, message string, status int, cause error) *AppError {
return &AppError{Code: code, Message: fmt.Sprintf("%s: %v", message, cause), Status: status}
}
// Common errors
var (
ErrBadRequest = &AppError{Code: "bad_request", Message: "잘못된 요청입니다", Status: 400}
ErrUnauthorized = &AppError{Code: "unauthorized", Message: "인증이 필요합니다", Status: 401}
ErrForbidden = &AppError{Code: "forbidden", Message: "권한이 없습니다", Status: 403}
ErrNotFound = &AppError{Code: "not_found", Message: "리소스를 찾을 수 없습니다", Status: 404}
ErrConflict = &AppError{Code: "conflict", Message: "이미 존재합니다", Status: 409}
ErrRateLimited = &AppError{Code: "rate_limited", Message: "요청이 너무 많습니다", Status: 429}
ErrInternal = &AppError{Code: "internal_error", Message: "서버 오류가 발생했습니다", Status: 500}
)
// BadRequest creates a 400 error with a custom message.
func BadRequest(message string) *AppError {
return &AppError{Code: "bad_request", Message: message, Status: 400}
}
// Unauthorized creates a 401 error with a custom message.
func Unauthorized(message string) *AppError {
return &AppError{Code: "unauthorized", Message: message, Status: 401}
}
// NotFound creates a 404 error with a custom message.
func NotFound(message string) *AppError {
return &AppError{Code: "not_found", Message: message, Status: 404}
}
// Conflict creates a 409 error with a custom message.
func Conflict(message string) *AppError {
return &AppError{Code: "conflict", Message: message, Status: 409}
}
// Internal creates a 500 error with a custom message.
func Internal(message string) *AppError {
return &AppError{Code: "internal_error", Message: message, Status: 500}
}
// ErrDuplicateUsername is returned when a username already exists.
var ErrDuplicateUsername = fmt.Errorf("이미 사용 중인 아이디입니다")
// IsDuplicateEntry checks if a GORM error is a MySQL duplicate key violation (error 1062).
func IsDuplicateEntry(err error) bool {
var mysqlErr *mysql.MySQLError
if errors.As(err, &mysqlErr) {
return mysqlErr.Number == 1062
}
return strings.Contains(err.Error(), "Duplicate entry") || strings.Contains(err.Error(), "UNIQUE constraint")
}