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 != "" }