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) } } } }