feat(errors,middlwares): add error handler middleware
This commit is contained in:
11
app/app.go
11
app/app.go
@@ -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
18
pages/error.templ
Normal 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>
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user