diff --git a/pkg/routes/api/v1/humaapi/errors.go b/pkg/routes/api/v1/humaapi/errors.go new file mode 100644 index 000000000..21c104401 --- /dev/null +++ b/pkg/routes/api/v1/humaapi/errors.go @@ -0,0 +1,29 @@ +package humaapi + +import ( + "github.com/danielgtaylor/huma/v2" +) + +// vikunjaError is the JSON shape Vikunja's existing error_handler.go emits +// for string-style errors. We preserve it so frontend contracts don't change. +type vikunjaError struct { + StatusCode int `json:"-"` + Code int `json:"code,omitempty"` + Message string `json:"message"` +} + +func (e *vikunjaError) Error() string { return e.Message } +func (e *vikunjaError) GetStatus() int { return e.StatusCode } + +// NewVikunjaError produces an error that serializes to Vikunja's legacy shape +// (`{"message": "..."}` for plain errors; `{"code": X, "message": "..."}` when +// a domain code is supplied). Registered as huma.NewError so every Huma +// handler's error return routes through here. +func NewVikunjaError(status int, msg string, errs ...error) huma.StatusError { + return &vikunjaError{StatusCode: status, Message: msg} +} + +// Install replaces huma.NewError globally. Call once at init. +func Install() { + huma.NewError = NewVikunjaError +} diff --git a/pkg/routes/api/v1/humaapi/errors_test.go b/pkg/routes/api/v1/humaapi/errors_test.go new file mode 100644 index 000000000..93eaf62bc --- /dev/null +++ b/pkg/routes/api/v1/humaapi/errors_test.go @@ -0,0 +1,31 @@ +package humaapi + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/danielgtaylor/huma/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVikunjaErrorShape_BasicCodeMessage(t *testing.T) { + err := NewVikunjaError(http.StatusForbidden, "Forbidden") + b, marshalErr := json.Marshal(err) + require.NoError(t, marshalErr) + + var got map[string]any + require.NoError(t, json.Unmarshal(b, &got)) + assert.Equal(t, "Forbidden", got["message"]) + // must not include RFC 9457 fields + _, hasType := got["type"] + _, hasTitle := got["title"] + assert.False(t, hasType, "unexpected RFC 9457 field 'type'") + assert.False(t, hasTitle, "unexpected RFC 9457 field 'title'") +} + +func TestVikunjaErrorShape_StatusCoderInterface(t *testing.T) { + var e huma.StatusError = NewVikunjaError(http.StatusNotFound, "not found") + assert.Equal(t, http.StatusNotFound, e.GetStatus()) +}