235 lines
5.5 KiB
Go
235 lines
5.5 KiB
Go
package rerrors
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"forge.capytal.company/capytalcode/project-comicverse/router/middleware"
|
|
"github.com/a-h/templ"
|
|
)
|
|
|
|
type RouteError struct {
|
|
StatusCode int `json:"status_code"`
|
|
Error string `json:"error"`
|
|
Info map[string]any `json:"info"`
|
|
}
|
|
|
|
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{}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
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(),
|
|
},
|
|
})
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
if _, err = w.Write(j); err != nil {
|
|
_, _ = w.Write([]byte("Failed to write error JSON string to body"))
|
|
}
|
|
return
|
|
}
|
|
|
|
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 ErrorMiddleware struct {
|
|
page ErrorMiddlewarePage
|
|
notfound ErrorMiddlewarePage
|
|
log *slog.Logger
|
|
}
|
|
|
|
func NewErrorMiddleware(
|
|
p ErrorMiddlewarePage,
|
|
l *slog.Logger,
|
|
notfound ...ErrorMiddlewarePage,
|
|
) *ErrorMiddleware {
|
|
var nf ErrorMiddlewarePage
|
|
if len(notfound) > 0 {
|
|
nf = notfound[0]
|
|
} else {
|
|
nf = p
|
|
}
|
|
|
|
l = l.WithGroup("error_middleware")
|
|
|
|
return &ErrorMiddleware{p, nf, l}
|
|
}
|
|
|
|
func (m *ErrorMiddleware) Wrap(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if uerr := r.URL.Query().Get("error"); uerr != "" {
|
|
e, err := base64.URLEncoding.DecodeString(uerr)
|
|
if err != nil {
|
|
m.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 {
|
|
m.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
|
|
}
|
|
|
|
w.WriteHeader(rerr.StatusCode)
|
|
if err := m.page(rerr).Render(r.Context(), w); err != nil {
|
|
_, _ = w.Write(e)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
mw := middleware.MultiResponseWriter(w, &buf)
|
|
|
|
next.ServeHTTP(mw, r)
|
|
|
|
if mw.Header().Get("Status") == "" {
|
|
m.log.Warn("Endpoint did not return a Status code",
|
|
slog.String("method", r.Method),
|
|
slog.String("path", r.URL.Path),
|
|
slog.String("status", mw.Header().Get("Status")),
|
|
slog.Any("data", buf),
|
|
)
|
|
return
|
|
}
|
|
|
|
status, err := strconv.Atoi(mw.Header().Get("Status"))
|
|
if err != nil {
|
|
m.log.Warn("Failed to parse Status code to a integer",
|
|
slog.String("method", r.Method),
|
|
slog.String("path", r.URL.Path),
|
|
slog.String("status", mw.Header().Get("Status")),
|
|
slog.String("data", err.Error()),
|
|
)
|
|
return
|
|
}
|
|
|
|
if status < 400 {
|
|
return
|
|
} else if status == 404 {
|
|
rerr := NotFound()
|
|
|
|
w.WriteHeader(rerr.StatusCode)
|
|
|
|
b, err := json.Marshal(rerr)
|
|
if err != nil {
|
|
_, _ = w.Write([]byte(
|
|
fmt.Sprintf("%#v", rerr),
|
|
))
|
|
return
|
|
}
|
|
|
|
if prefersHtml(r.Header) {
|
|
u, _ := url.Parse(r.URL.String())
|
|
|
|
q := u.Query()
|
|
q.Add("error", base64.URLEncoding.EncodeToString(b))
|
|
u.RawQuery = q.Encode()
|
|
|
|
http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect)
|
|
return
|
|
}
|
|
|
|
_, _ = w.Write(b)
|
|
|
|
return
|
|
}
|
|
|
|
body, err := io.ReadAll(&buf)
|
|
if err != nil {
|
|
m.log.Error("Failed to read from error body",
|
|
slog.String("method", r.Method),
|
|
slog.String("path", r.URL.Path),
|
|
slog.String("status", mw.Header().Get("Status")),
|
|
slog.Any("data", buf),
|
|
)
|
|
return
|
|
}
|
|
|
|
if mw.Header().Get("Content-Type") != "application/json" {
|
|
m.log.Warn("Endpoint didn't return a structured error",
|
|
slog.String("method", r.Method),
|
|
slog.String("path", r.URL.Path),
|
|
slog.String("status", mw.Header().Get("Status")),
|
|
slog.String("data", string(body)),
|
|
)
|
|
return
|
|
}
|
|
|
|
if prefersHtml(r.Header) {
|
|
u, _ := url.Parse(r.URL.String())
|
|
|
|
q := u.Query()
|
|
q.Add("error", base64.URLEncoding.EncodeToString(body))
|
|
u.RawQuery = q.Encode()
|
|
|
|
http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect)
|
|
}
|
|
})
|
|
}
|
|
|
|
func prefersHtml(h http.Header) bool {
|
|
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")
|
|
}
|