feat(api/v2): preserve Vikunja numeric error code in problem+json

translateDomainError discarded web.HTTPError.Code, so v2 error bodies
always read code 0 — losing the v1 contract the error docs key off.
Override huma.NewError with a VikunjaErrorModel that adds a code field,
so both the generated OpenAPI schema and runtime responses carry it.
Domain errors with a numeric code now surface it (e.g. 8002 for a
missing label, matching v1); errors without one omit it.
This commit is contained in:
kolaente 2026-05-30 22:54:14 +02:00 committed by kolaente
parent e257823cef
commit 8532016a2d
1 changed files with 44 additions and 1 deletions

View File

@ -19,6 +19,7 @@ package apiv2
import (
"context"
"errors"
"net/http"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/modules/auth"
@ -57,7 +58,49 @@ func translateDomainError(err error) error {
if msg == "" {
msg = err.Error()
}
return huma.NewError(details.HTTPCode, msg)
se := huma.NewError(details.HTTPCode, msg)
// Preserve Vikunja's numeric domain error code (the value the
// error docs key off) on the problem+json body. v1 exposes it as
// `code`; without this v2 clients always read 0.
if vm, ok := se.(*vikunjaErrorModel); ok {
vm.Code = details.Code
}
return se
}
return err
}
// vikunjaErrorModel extends Huma's RFC 9457 body with Vikunja's numeric
// domain error code, preserving the v1 error-code contract on v2. Wired in
// as the global error type via the huma.NewError override in init().
type vikunjaErrorModel struct {
huma.ErrorModel
Code int `json:"code,omitempty" doc:"Vikunja numeric error code; see https://vikunja.io/docs/errors/"`
}
func init() {
// Replace Huma's default error constructor so both the generated
// OpenAPI schema and runtime responses use vikunjaErrorModel. Huma
// derives the error-response schema from NewError(0, "") at register
// time and routes runtime errors through the same constructor, so the
// `code` field stays consistent between spec and wire.
huma.NewError = func(status int, msg string, errs ...error) huma.StatusError {
details := make([]*huma.ErrorDetail, 0, len(errs))
for _, e := range errs {
if e == nil {
continue
}
if d, ok := e.(huma.ErrorDetailer); ok {
details = append(details, d.ErrorDetail())
} else {
details = append(details, &huma.ErrorDetail{Message: e.Error()})
}
}
return &vikunjaErrorModel{ErrorModel: huma.ErrorModel{
Status: status,
Title: http.StatusText(status),
Detail: msg,
Errors: details,
}}
}
}