package rerrors import ( "encoding/base64" "encoding/json" "fmt" "log/slog" "net/http" "strings" "forge.capytal.company/loreddev/x/groute/middleware" "github.com/a-h/templ" ) const ( ERROR_MIDDLEWARE_HEADER = "XX-Error-Middleware" ERROR_VALUE_HEADER = "X-Error-Value" ) type RouteError struct { StatusCode int `json:"status_code"` Error string `json:"error"` Info map[string]any `json:"info"` Endpoint string } func NewRouteError(status int, error string, info ...map[string]any) RouteError { rerr := RouteError{StatusCode: status, Error: error} if len(info) > 0 { rerr.Info = info[0] } else { rerr.Info = map[string]any{} } return rerr } func (rerr RouteError) ServeHTTP(w http.ResponseWriter, r *http.Request) { if rerr.StatusCode == 0 { rerr.StatusCode = http.StatusNotImplemented } if rerr.Error == "" { rerr.Error = "MISSING ERROR DESCRIPTION" } if rerr.Info == nil { rerr.Info = map[string]any{} } j, err := json.Marshal(rerr) if err != nil { j, _ = json.Marshal(RouteError{ StatusCode: http.StatusInternalServerError, Error: "Failed to marshal error message to JSON", Info: map[string]any{ "source_value": fmt.Sprintf("%#v", rerr), "error": err.Error(), }, }) } if r.Header.Get(ERROR_MIDDLEWARE_HEADER) == "enable" && prefersHtml(r.Header) { q := r.URL.Query() q.Set("error", base64.URLEncoding.EncodeToString(j)) r.URL.RawQuery = q.Encode() http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(rerr.StatusCode) if _, err = w.Write(j); err != nil { _, _ = w.Write([]byte("Failed to write error JSON string to body")) } } type ErrorMiddlewarePage func(err RouteError) templ.Component type ErrorDisplayer struct { log *slog.Logger page ErrorMiddlewarePage } func (h ErrorDisplayer) ServeHTTP(w http.ResponseWriter, r *http.Request) { e, err := base64.URLEncoding.DecodeString(r.URL.Query().Get("error")) if err != nil { h.log.Error("Failed to decode \"error\" parameter from error redirect", slog.String("method", r.Method), slog.String("path", r.URL.Path), slog.Int("status", 0), slog.String("data", string(e)), ) w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte( fmt.Sprintf("Data %s\nError %s", string(e), err.Error()), )) return } var rerr RouteError if err := json.Unmarshal(e, &rerr); err != nil { h.log.Error("Failed to decode \"error\" parameter from error redirect", slog.String("method", r.Method), slog.String("path", r.URL.Path), slog.Int("status", 0), slog.String("data", string(e)), ) w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte( fmt.Sprintf("Data %s\nError %s", string(e), err.Error()), )) return } if rerr.Endpoint == "" { q := r.URL.Query() q.Del("error") r.URL.RawQuery = q.Encode() rerr.Endpoint = r.URL.String() } w.WriteHeader(rerr.StatusCode) if err := h.page(rerr).Render(r.Context(), w); err != nil { _, _ = w.Write(e) } } func NewErrorMiddleware( p ErrorMiddlewarePage, l *slog.Logger, notfound ...ErrorMiddlewarePage, ) middleware.Middleware { var nf ErrorMiddlewarePage if len(notfound) > 0 { nf = notfound[0] } else { nf = p } l = l.WithGroup("error_middleware") return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { r.Header.Set(ERROR_MIDDLEWARE_HEADER, "enable") if uerr := r.URL.Query().Get("error"); uerr != "" && prefersHtml(r.Header) { ErrorDisplayer{l, nf}.ServeHTTP(w, r) return } next.ServeHTTP(w, r) }) } } func prefersHtml(h http.Header) bool { if h.Get("Accept") == "" { return false } return (strings.Contains(h.Get("Accept"), "text/html") || strings.Contains(h.Get("Accept"), "application/xhtml+xml") || strings.Contains(h.Get("Accept"), "application/xml")) && !strings.Contains(h.Get("Accept"), "application/json") }