package echo
import (
"bytes"
"fmt"
"html/template"
"io/fs"
"net/http"
"github.com/elnormous/contenttype"
"github.com/labstack/echo/v4"
)
// TODO: This should allow plugging in other content types
// TODO: This should also be refactored into something prettier
func ErrorHandler(templates fs.FS, funcs template.FuncMap) func(error, echo.Context) {
handleHtml := func(c echo.Context, he *echo.HTTPError) error {
t, err := template.New("").Funcs(funcs).ParseFS(
templates,
"404.tpl",
"40x.tpl",
"50x.tpl",
"header.tpl",
"footer.tpl",
)
if err != nil {
return err
}
path := "50x.tpl"
if he.Code == 404 {
path = "404.tpl"
} else if he.Code >= 400 && he.Code <= 499 {
path = "40x.tpl"
}
buf := bytes.Buffer{}
if err = t.ExecuteTemplate(&buf, path, nil); err != nil {
err = c.String(he.Code, fmt.Sprintf("%s", he.Message))
}
return c.HTMLBlob(he.Code, buf.Bytes())
}
handlePlain := func(c echo.Context, he *echo.HTTPError) error {
return c.String(he.Code, fmt.Sprintf("%s", he.Message))
}
handleJson := func(c echo.Context, he *echo.HTTPError) error {
code := he.Code
message := he.Message
if m, ok := he.Message.(string); ok {
if c.Echo().Debug {
message = echo.Map{"message": m, "error": he.Error()}
} else {
message = echo.Map{"message": m}
}
}
return c.JSON(code, message)
}
errorWhileErroring := func(c echo.Context, err interface{}) {
c.Echo().Logger.Error(err)
if c.Echo().Debug {
c.JSON(http.StatusInternalServerError, &echo.HTTPError{
Code: http.StatusInternalServerError,
Message: fmt.Sprintf("Error while processing error page. %w", err),
})
} else {
c.JSON(http.StatusInternalServerError, &echo.HTTPError{
Code: http.StatusInternalServerError,
Message: http.StatusText(http.StatusInternalServerError),
})
}
}
handlers := map[string]func(echo.Context, *echo.HTTPError) error{
"text/plain": handlePlain,
"text/html": handleHtml,
"text/json": handleJson,
"application/json": handleJson,
}
// This is hand maintained here because order is important for negotiation,
// especially in the case of */*
hIndex := []contenttype.MediaType{
contenttype.NewMediaType("text/json"),
contenttype.NewMediaType("application/json"),
contenttype.NewMediaType("text/plain"),
contenttype.NewMediaType("text/html"),
}
return func(err error, c echo.Context) {
defer func() {
if r := recover(); r != nil {
errorWhileErroring(c, r)
}
}()
he, ok := err.(*echo.HTTPError)
if ok {
if he.Internal != nil {
if herr, ok := he.Internal.(*echo.HTTPError); ok {
he = herr
}
}
} else {
he = &echo.HTTPError{
Code: http.StatusInternalServerError,
Message: http.StatusText(http.StatusInternalServerError),
}
}
ct, _, err := contenttype.GetAcceptableMediaType(c.Request(), hIndex)
if err != nil {
c.Echo().Logger.Error("Error negotiating content type in error handler, using json")
ct = contenttype.NewMediaType("text/json")
}
handle, ok := handlers[ct.String()]
if !ok {
c.Echo().Logger.Errorf("Error handler content type %s is unknown", ct.String())
handle = handleJson
}
if !c.Response().Committed {
if c.Request().Method == http.MethodHead { // Issue #608
err = c.NoContent(he.Code)
} else {
err = handle(c, he)
}
if err != nil {
errorWhileErroring(c, err)
}
}
}
}