feat(errors,middlwares): add error handler middleware

This commit is contained in:
Guz
2024-10-17 23:47:35 -03:00
parent f3f060ddc8
commit c55a516a3d
5 changed files with 250 additions and 4 deletions

View File

@@ -11,6 +11,7 @@ import (
devPages "forge.capytal.company/capytalcode/project-comicverse/pages/dev"
"forge.capytal.company/capytalcode/project-comicverse/router"
"forge.capytal.company/capytalcode/project-comicverse/router/middleware"
"forge.capytal.company/capytalcode/project-comicverse/router/rerrors"
)
type App struct {
@@ -60,16 +61,18 @@ func (a *App) Run() {
if a.dev {
router.HandleRoutes(devPages.PAGES)
router.AddMiddleware(middleware.DevMiddleware)
}
logger := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
mlogger := middleware.NewLoggerMiddleware(slog.New(logger))
}))
mlogger := middleware.NewLoggerMiddleware(logger)
router.AddMiddleware(mlogger.Wrap)
mErrors := rerrors.NewErrorMiddleware(pages.ErrorPage{}.Component, logger)
router.AddMiddleware(mErrors.Wrap)
if err := http.ListenAndServe(fmt.Sprintf(":%v", a.port), router); err != nil {
log.Fatal(err)
}

18
pages/error.templ Normal file
View File

@@ -0,0 +1,18 @@
package pages
import (
"forge.capytal.company/capytalcode/project-comicverse/router/rerrors"
"forge.capytal.company/capytalcode/project-comicverse/templates/layouts"
"fmt"
)
type ErrorPage struct{}
templ (p ErrorPage) Component(err rerrors.RouteError) {
@layouts.Page() {
<main>
<h1>Error</h1>
<p>{ fmt.Sprintf("%#v", err) }</p>
</main>
}
}

View File

@@ -3,6 +3,7 @@ package middleware
import (
"errors"
"fmt"
"io"
"net/http"
"strconv"
)
@@ -60,3 +61,50 @@ func (m *MiddlewaredReponse) ReallyWriteHeader() (int, error) {
return bytes, nil
}
type multiResponseWriter struct {
response http.ResponseWriter
writers []io.Writer
}
func MultiResponseWriter(
w http.ResponseWriter,
writers ...io.Writer,
) http.ResponseWriter {
if mw, ok := w.(*multiResponseWriter); ok {
mw.writers = append(mw.writers, writers...)
return mw
}
allWriters := make([]io.Writer, 0, len(writers))
for _, iow := range writers {
if mw, ok := iow.(*multiResponseWriter); ok {
allWriters = append(allWriters, mw.writers...)
} else {
allWriters = append(allWriters, iow)
}
}
return &multiResponseWriter{w, allWriters}
}
func (w *multiResponseWriter) WriteHeader(status int) {
w.response.WriteHeader(status)
}
func (w *multiResponseWriter) Write(p []byte) (int, error) {
for _, w := range w.writers {
n, err := w.Write(p)
if err != nil {
return n, err
}
if n != len(p) {
return n, io.ErrShortWrite
}
}
return w.response.Write(p)
}
func (w *multiResponseWriter) Header() http.Header {
return w.response.Header()
}

View File

@@ -2,6 +2,10 @@ package rerrors
import "net/http"
func NotFound() RouteError {
return NewRouteError(http.StatusNotFound, "Not Found", map[string]any{})
}
func MissingParameters(params []string) RouteError {
return NewRouteError(http.StatusBadRequest, "Missing parameters", map[string]any{
"missing_parameters": params,

View File

@@ -1,9 +1,19 @@
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 {
@@ -59,3 +69,166 @@ func (rerr RouteError) ServeHTTP(w http.ResponseWriter, r *http.Request) {
_, _ = 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")
}