Найдено статическим анализом (dupl/jscpd) + проверено вручную:
Go:
- appstate/path.go: ResolveDBPath/ResolveConfigPath → общий resolvePath()
- handlers/respond.go: дженерик BindJSON[T] для bind+422-ошибки
- article/generator.go: buildNetSegment/buildPSUSegment → buildProfileSegment()
- cmd/qfs/main.go (+respond.go): 8 повторяющихся switch{case errors.Is(...)}
→ respondByErrCase(c, err, errCase{...}, ...)
Frontend (index.html):
- 5 идентичных обработчиков клавиатурной навигации автокомплита
→ handleAutocompleteKeyGeneric(event, onSelect)
- 4 функции сборки нового элемента корзины из автокомплита
→ buildNewCartItem()/commitCartChange()
Также: PricingMarkup — единая точка правды для аплифт-коэффициента
в JS (render + export), с cross-reference комментариями к Go-константам
в export.go (defaultSaleMarkup/stockCompetitorMarkupFactor).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
86 lines
1.7 KiB
Go
86 lines
1.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func RespondError(c *gin.Context, status int, fallback string, err error) {
|
|
if err != nil {
|
|
_ = c.Error(err)
|
|
}
|
|
c.JSON(status, gin.H{"error": clientFacingErrorMessage(status, fallback, err)})
|
|
}
|
|
|
|
// BindJSON decodes the request body into T, responding with a 422 and writing
|
|
// the error via RespondError on failure. The second return value reports success.
|
|
func BindJSON[T any](c *gin.Context) (*T, bool) {
|
|
var req T
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
RespondError(c, http.StatusUnprocessableEntity, "invalid request", err)
|
|
return nil, false
|
|
}
|
|
return &req, true
|
|
}
|
|
|
|
func clientFacingErrorMessage(status int, fallback string, err error) string {
|
|
if err == nil {
|
|
return fallback
|
|
}
|
|
if status >= 500 {
|
|
return fallback
|
|
}
|
|
if isRequestDecodeError(err) {
|
|
return fallback
|
|
}
|
|
|
|
message := strings.TrimSpace(err.Error())
|
|
if message == "" {
|
|
return fallback
|
|
}
|
|
if looksTechnicalError(message) {
|
|
return fallback
|
|
}
|
|
return message
|
|
}
|
|
|
|
func isRequestDecodeError(err error) bool {
|
|
var syntaxErr *json.SyntaxError
|
|
if errors.As(err, &syntaxErr) {
|
|
return true
|
|
}
|
|
|
|
var unmarshalTypeErr *json.UnmarshalTypeError
|
|
if errors.As(err, &unmarshalTypeErr) {
|
|
return true
|
|
}
|
|
|
|
return errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, io.EOF)
|
|
}
|
|
|
|
func looksTechnicalError(message string) bool {
|
|
lower := strings.ToLower(strings.TrimSpace(message))
|
|
needles := []string{
|
|
"sql",
|
|
"gorm",
|
|
"driver",
|
|
"constraint",
|
|
"syntax error",
|
|
"unexpected eof",
|
|
"record not found",
|
|
"no such table",
|
|
"stack trace",
|
|
}
|
|
for _, needle := range needles {
|
|
if strings.Contains(lower, needle) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|