132 lines
3.6 KiB
Go
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 != ""
|
|
}
|