From d76c0098083c20671d1dd45e9dd678a7fcc5d35a Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 5 Jun 2026 10:13:21 +0200 Subject: [PATCH] fix(api/v2): map ValidationHTTPError to its HTTP status translateDomainError only recognized web.HTTPErrorProcessor, so a ValidationHTTPError from InvalidFieldError (e.g. an unknown webhook event) leaked as a 500 instead of the 412 v1 returns. It carries the status via GetHTTPCode() but cannot implement HTTPErrorProcessor because the embedded web.HTTPError field shadows the method name. Add a GetHTTPCode/GetCode branch so v2 surfaces the right status and preserves the v1 numeric code on the body. --- pkg/models/error.go | 7 +++++++ pkg/routes/api/v2/errors.go | 26 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/pkg/models/error.go b/pkg/models/error.go index 92c11fd5f..bd15c2284 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -131,6 +131,13 @@ func (err ValidationHTTPError) GetHTTPCode() int { return err.HTTPCode } +// GetCode returns Vikunja's numeric domain error code. v2's translateDomainError +// reads it to keep the v1 `code` body contract, since this type does not +// implement web.HTTPErrorProcessor (the embedded field shadows the method name). +func (err ValidationHTTPError) GetCode() int { + return err.Code +} + func InvalidFieldError(fields []string) error { return InvalidFieldErrorWithMessage(fields, "Invalid Data") } diff --git a/pkg/routes/api/v2/errors.go b/pkg/routes/api/v2/errors.go index f3542024f..a11ca25f7 100644 --- a/pkg/routes/api/v2/errors.go +++ b/pkg/routes/api/v2/errors.go @@ -43,6 +43,21 @@ func authFromCtx(ctx context.Context) (web.Auth, error) { return a, nil } +// httpCodeGetter is satisfied by validation errors that carry an HTTP status +// without the HTTPErrorProcessor method — notably models.ValidationHTTPError, +// returned by InvalidFieldError. v1's error handler reads GetHTTPCode() off these; +// without the same branch here they'd fall through to a 500. +type httpCodeGetter interface { + GetHTTPCode() int +} + +// domainCodeGetter exposes Vikunja's numeric domain error code on errors that +// are not HTTPErrorProcessors (again, ValidationHTTPError) so v2 can keep the v1 +// `code` body contract. +type domainCodeGetter interface { + GetCode() int +} + // translateDomainError maps a Vikunja domain error (web.HTTPErrorProcessor) // onto Huma's status-error type so the response carries the right code // and an RFC 9457 body. Errors without HTTP semantics fall through, which @@ -67,6 +82,17 @@ func translateDomainError(err error) error { } return se } + var cg httpCodeGetter + if errors.As(err, &cg) { + se := huma.NewError(cg.GetHTTPCode(), err.Error()) + if vm, ok := se.(*vikunjaErrorModel); ok { + var dc domainCodeGetter + if errors.As(err, &dc) { + vm.Code = dc.GetCode() + } + } + return se + } return err }