Initial project snapshot
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func WriteJSON(w http.ResponseWriter, status int, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
|
||||
func WriteError(w http.ResponseWriter, status int, message string) {
|
||||
traceID := ensureTraceID(w)
|
||||
WriteJSON(w, status, ErrorResponse{
|
||||
Error: NewErrorMessage(status, message, nil, traceID),
|
||||
})
|
||||
}
|
||||
|
||||
func WriteErrorMessage(w http.ResponseWriter, status int, message any) {
|
||||
traceID := ensureTraceID(w)
|
||||
switch payload := message.(type) {
|
||||
case string:
|
||||
WriteJSON(w, status, ErrorResponse{
|
||||
Error: NewErrorMessage(status, payload, nil, traceID),
|
||||
})
|
||||
case ErrorResponse:
|
||||
payload.Error.TraceID = traceID
|
||||
WriteJSON(w, status, payload)
|
||||
case *ErrorResponse:
|
||||
if payload == nil {
|
||||
WriteJSON(w, status, ErrorResponse{
|
||||
Error: NewErrorMessage(status, "", nil, traceID),
|
||||
})
|
||||
return
|
||||
}
|
||||
payload.Error.TraceID = traceID
|
||||
WriteJSON(w, status, payload)
|
||||
default:
|
||||
WriteJSON(w, status, ErrorResponse{
|
||||
Error: NewErrorMessage(status, "Request failed.", nil, traceID),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
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 != ""
|
||||
}
|
||||
Reference in New Issue
Block a user