Files
vikunja/pkg/routes/error_handler.go
renovate[bot] 9a61453e86 fix(deps): update module github.com/labstack/echo/v4 to v5 (#2131)
Closes https://github.com/go-vikunja/vikunja/pull/2133

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: kolaente <k@knt.li>
2026-01-24 20:38:32 +01:00

142 lines
5.0 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package routes
import (
"encoding/json"
"errors"
"net/http"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/web"
"github.com/getsentry/sentry-go"
"github.com/labstack/echo/v5"
)
// httpCodeGetter is an interface for errors that can provide their HTTP status code.
type httpCodeGetter interface {
GetHTTPCode() int
}
// errorMessage is used to wrap string error messages in a consistent JSON structure.
type errorMessage struct {
Message interface{} `json:"message"`
}
// CreateHTTPErrorHandler creates a centralized HTTP error handler that:
// 1. Converts all error types to proper HTTP responses
// 2. Preserves full error details (like ValidationHTTPError.InvalidFields)
// 3. Handles Sentry reporting for 5xx errors
// 4. Logs all errors appropriately
func CreateHTTPErrorHandler(e *echo.Echo, enableSentry bool) echo.HTTPErrorHandler {
return func(c *echo.Context, err error) {
// Check if the response has already been committed (e.g., by the RequestLogger middleware
// with HandleError=true). If so, we should not try to write another response.
if r, _ := echo.UnwrapResponse(c.Response()); r != nil && r.Committed {
return
}
var (
code = http.StatusInternalServerError
message interface{} = http.StatusText(http.StatusInternalServerError)
)
// Keep track of the original error for logging/sentry
originalErr := err
// 1. Check if it implements HTTPStatusCoder (includes echo.ErrForbidden, etc.)
// In Echo v5, predefined errors like ErrForbidden are *httpError (unexported),
// not *HTTPError, so we must check the interface instead of the concrete type.
var sc echo.HTTPStatusCoder
if errors.As(err, &sc) {
code = sc.StatusCode()
// HTTPStatusCoder doesn't have Error(), so we use the status text
message = http.StatusText(code)
}
// 2. If it's specifically an HTTPError, use its message for more details
var he *echo.HTTPError
if errors.As(err, &he) {
code = he.Code
if he.Message != "" {
message = he.Message
}
}
// 3. Special case: 413 body limit → convert to ErrFileIsTooLarge
// Check both the code (if it was an HTTPError) and errors.Is for wrapped errors
// In Echo v5, body limit errors during multipart parsing may be wrapped
if code == http.StatusRequestEntityTooLarge || errors.Is(err, echo.ErrStatusRequestEntityTooLarge) {
fileErr := files.ErrFileIsTooLarge{}
errDetails := fileErr.HTTPError()
code = errDetails.HTTPCode
message = errDetails
} else if _, isMarshaler := err.(json.Marshaler); isMarshaler {
// 4. Check for json.Marshaler (preserves full struct like ValidationHTTPError)
// This allows errors with extra fields (like InvalidFields) to be serialized correctly
if codeGetter, hasCode := err.(httpCodeGetter); hasCode {
code = codeGetter.GetHTTPCode()
}
message = err // Echo will serialize via MarshalJSON
} else if hp, ok := err.(web.HTTPErrorProcessor); ok {
// 5. Standard HTTPErrorProcessor (domain errors like ErrProjectDoesNotExist)
errDetails := hp.HTTPError()
code = errDetails.HTTPCode
message = errDetails
}
// 6. For any other error type, we keep the defaults (500 with generic message)
// or the echo.HTTPStatusCoder/HTTPError values if it was that type
// Sentry reporting for 5xx errors
if enableSentry && code >= 500 {
reportToSentry(originalErr, c)
}
// Send response
if c.Request().Method == http.MethodHead {
err = c.NoContent(code)
} else {
// Wrap string messages in a struct to ensure consistent JSON format
// e.g., "Forbidden" becomes {"message": "Forbidden"}
if _, isString := message.(string); isString {
message = errorMessage{Message: message}
}
err = c.JSON(code, message)
}
if err != nil {
e.Logger.Error(err.Error())
}
}
}
// reportToSentry sends an error to Sentry with request context
func reportToSentry(err error, c *echo.Context) {
hub := GetSentryHubFromContext(c)
if hub != nil {
hub.WithScope(func(scope *sentry.Scope) {
scope.SetExtra("url", c.Request().URL)
hub.CaptureException(err)
})
} else {
sentry.CaptureException(err)
log.Debugf("Could not add context for sending error '%s' to sentry", err.Error())
}
log.Debugf("Error '%s' sent to sentry", err.Error())
}