package echo import ( "bytes" "fmt" "html/template" "net/http" "github.com/elnormous/contenttype" "github.com/labstack/echo/v4" ) type ContentErrorHandler interface { Handle(echo.Context, *echo.HTTPError) error } type ErrorHandler interface { AddHandler(ContentErrorHandler, ...string) HandleError(error, echo.Context) } type HTMLErrorHandler struct { r *TemplateRenderer fallback *template.Template } var _ ContentErrorHandler = (*HTMLErrorHandler)(nil) func NewHTMLErrorHandler(r *TemplateRenderer) *HTMLErrorHandler { t, err := template.New("").Parse("

Error

{{ .Message }}
\n") if err != nil { panic("NewHTMLErrorHandler: error parsing fallback template") } return &HTMLErrorHandler{r: r, fallback: t} } func (h *HTMLErrorHandler) Handle(c echo.Context, e *echo.HTTPError) error { path := "50x.tpl" if e.Code == 404 { path = "404.tpl" } else if e.Code >= 400 && e.Code <= 499 { path = "40x.tpl" } buf := bytes.Buffer{} if !h.r.HaveTemplate(path) { c.Logger().Errorf("Error template %s is missing, using fallback", path) if err := h.fallback.Execute(&buf, e); err != nil { c.Logger().Errorf("Error rendering HTML error page: %s", err) return c.String(e.Code, e.Error()) } } else { // Must pass nil as the data value otherwise complex templates fail if err := h.r.Render(&buf, path, nil, c); err != nil { c.Logger().Errorf("Error rendering HTML error page: %s", err) return c.String(e.Code, e.Error()) } } return c.HTMLBlob(e.Code, buf.Bytes()) } type PlainTextErrorHandler struct{} var _ ContentErrorHandler = (*PlainTextErrorHandler)(nil) func (h *PlainTextErrorHandler) Handle(c echo.Context, e *echo.HTTPError) error { return c.String(e.Code, fmt.Sprintf("%s", e.Message)) } type JSONErrorHandler struct{} var _ ContentErrorHandler = (*JSONErrorHandler)(nil) func (h *JSONErrorHandler) Handle(c echo.Context, e *echo.HTTPError) error { code := e.Code message := e.Message if m, ok := e.Message.(string); ok { if c.Echo().Debug { message = echo.Map{"message": m, "error": e.Error()} } else { message = echo.Map{"message": m} } } return c.JSON(code, message) } type failsafeHandler struct{} var _ ContentErrorHandler = (*failsafeHandler)(nil) func (h *failsafeHandler) Handle(c echo.Context, e *echo.HTTPError) error { c.Echo().Logger.Error("Error while processing error page: %s", e) c.JSON(http.StatusInternalServerError, e) return nil } type EchoErrorHandler struct { types []contenttype.MediaType typeMap map[string]ContentErrorHandler failsafeHandler ContentErrorHandler } var _ ErrorHandler = (*EchoErrorHandler)(nil) func NewEchoErrorHandler() *EchoErrorHandler { return &EchoErrorHandler{ types: []contenttype.MediaType{}, typeMap: map[string]ContentErrorHandler{}, failsafeHandler: &failsafeHandler{}, } } func NewDefaultErrorHandler(tr *TemplateRenderer) *EchoErrorHandler { h := NewEchoErrorHandler() // The order of these is important, especially when negotiating */* h.AddHandler(&JSONErrorHandler{}, "text/json", "application/json") h.AddHandler(&PlainTextErrorHandler{}, "text/plain") h.AddHandler(NewHTMLErrorHandler(tr), "text/html") return h } func NewNoHTMLErrorHandler() *EchoErrorHandler { h := NewEchoErrorHandler() // The order of these is important, especially when negotiating */* h.AddHandler(&JSONErrorHandler{}, "text/json", "application/json") h.AddHandler(&PlainTextErrorHandler{}, "text/plain") return h } func (h *EchoErrorHandler) AddHandler(eh ContentErrorHandler, contentTypes ...string) { for _, ct := range contentTypes { h.types = append(h.types, contenttype.NewMediaType(ct)) h.typeMap[ct] = eh } } func (h *EchoErrorHandler) castToHttpError(e error) *echo.HTTPError { he, ok := e.(*echo.HTTPError) if ok { if he.Internal != nil { if ihe, ok := he.Internal.(*echo.HTTPError); ok { return ihe } } return he } else { return &echo.HTTPError{ Code: http.StatusInternalServerError, Message: http.StatusText(http.StatusInternalServerError), } } } func (h *EchoErrorHandler) negotiateContentType(r *http.Request) (ContentErrorHandler, error) { var err error ct, _, err := contenttype.GetAcceptableMediaType(r, h.types) if err != nil { err = fmt.Errorf("Error negotiating content type in error handler, falling back to JSON: %w", err) ct = contenttype.NewMediaType("text/json") } handle, found := h.typeMap[ct.String()] if !found { err = fmt.Errorf("Unable to find handler for content type: %s", ct) handle = h.failsafeHandler } return handle, err } func (h *EchoErrorHandler) HandleError(err error, c echo.Context) { defer func() { if r := recover(); r != nil { if err, ok := r.(error); !ok { h.failsafeHandler.Handle(c, h.castToHttpError(fmt.Errorf("%s", err))) } else { h.failsafeHandler.Handle(c, h.castToHttpError(err)) } } }() logger := c.Echo().Logger he := h.castToHttpError(err) handle, cterr := h.negotiateContentType(c.Request()) if cterr != nil { logger.Errorf("%s", cterr) } if !c.Response().Committed { if c.Request().Method == http.MethodHead { // Issue #608 err = c.NoContent(he.Code) } else { err = handle.Handle(c, he) } if err != nil { h.failsafeHandler.Handle(c, h.castToHttpError(err)) } } else { logger.Errorf("Error occurred in committed response: %s", he) } }