Files
rdp-proxy/backend/internal/platform/httpx/message.go
T
2026-04-28 22:29:50 +03:00

132 lines
3.6 KiB
Go

package httpx
import (
"net/http"
"strings"
"unicode"
"github.com/google/uuid"
messagecontracts "github.com/example/remote-access-platform/backend/pkg/contracts/message"
)
type ErrorResponse struct {
Error messagecontracts.Message `json:"error"`
}
func NewMessage(code, messageKey, fallbackMessage string, details map[string]any, traceID string) messagecontracts.Message {
if traceID == "" {
traceID = uuid.NewString()
}
if details == nil {
details = map[string]any{}
}
return messagecontracts.Message{
Code: code,
MessageKey: messageKey,
FallbackMessage: fallbackMessage,
Details: details,
TraceID: traceID,
}
}
func NewErrorMessage(status int, fallbackMessage string, details map[string]any, traceID string) messagecontracts.Message {
normalizedFallback, normalizedDetails := normalizeErrorFallback(status, fallbackMessage, details)
code := deriveErrorCode(status, normalizedFallback)
return NewMessage(code, "errors."+code, normalizedFallback, normalizedDetails, traceID)
}
func ensureTraceID(w http.ResponseWriter) string {
traceID := w.Header().Get("X-Trace-Id")
if traceID == "" {
traceID = uuid.NewString()
w.Header().Set("X-Trace-Id", traceID)
}
return traceID
}
func normalizeErrorFallback(status int, fallbackMessage string, details map[string]any) (string, map[string]any) {
if details == nil {
details = map[string]any{}
}
details["http_status"] = status
if status >= http.StatusInternalServerError {
return "An internal server error occurred.", details
}
trimmed := strings.TrimSpace(fallbackMessage)
switch strings.ToLower(trimmed) {
case "forbidden", "access denied":
return "Access denied.", details
}
if field, ok := extractRequiredField(trimmed); ok {
details["field"] = field
}
return trimmed, details
}
func deriveErrorCode(status int, fallbackMessage string) string {
switch strings.ToLower(strings.TrimSpace(fallbackMessage)) {
case "invalid credentials":
return "auth.invalid_credentials"
case "session expired. please sign in again.":
return "auth.session_expired"
case "access denied.":
return "common.access_denied"
}
statusPrefix := map[int]string{
http.StatusBadRequest: "bad_request",
http.StatusUnauthorized: "unauthorized",
http.StatusForbidden: "forbidden",
http.StatusNotFound: "not_found",
http.StatusConflict: "conflict",
http.StatusUnprocessableEntity: "unprocessable_entity",
http.StatusInternalServerError: "internal_server_error",
}[status]
if statusPrefix == "" {
statusPrefix = "http_" + strings.ReplaceAll(http.StatusText(status), " ", "_")
statusPrefix = strings.ToLower(statusPrefix)
}
slug := slugifyMessage(fallbackMessage)
if slug == "" {
slug = "message"
}
if status >= http.StatusInternalServerError {
return "common." + statusPrefix
}
return statusPrefix + "." + slug
}
func slugifyMessage(input string) string {
var builder strings.Builder
lastUnderscore := false
for _, r := range strings.ToLower(strings.TrimSpace(input)) {
if unicode.IsLetter(r) || unicode.IsDigit(r) {
builder.WriteRune(r)
lastUnderscore = false
continue
}
if !lastUnderscore {
builder.WriteRune('_')
lastUnderscore = true
}
}
return strings.Trim(builder.String(), "_")
}
func extractRequiredField(message string) (string, bool) {
const suffix = " is required"
if !strings.HasSuffix(strings.ToLower(message), suffix) {
return "", false
}
field := strings.TrimSpace(message[:len(message)-len(suffix)])
field = strings.ReplaceAll(field, " ", "_")
field = strings.ToLower(field)
return field, field != ""
}