feat(context): unwrap joined errors in Context.Error()

errors.Join() produces a single error implementing Unwrap() []error.
Previously, c.Error(errors.Join(e1, e2)) stored the joined error as one
entry, confusing errorMsgs output. Now each unwrapped error becomes an
individual entry, with gin.Error type preserved when applicable.

Fixes #4237
This commit is contained in:
vierblatt 2026-07-05 02:08:51 +08:00
parent 34dac209ff
commit 9cebe2dc89
2 changed files with 66 additions and 0 deletions

View File

@ -264,6 +264,17 @@ func (c *Context) Error(err error) *Error {
panic("err is nil")
}
// Unwrap joined errors (e.g. from errors.Join), adding each individually
if joined, ok := err.(interface{ Unwrap() []error }); ok {
if errs := joined.Unwrap(); len(errs) > 0 {
var last *Error
for _, e := range errs {
last = c.Error(e)
}
return last
}
}
var parsedError *Error
ok := errors.As(err, &parsedError)
if !ok {

View File

@ -2048,6 +2048,61 @@ func TestContextTypedError(t *testing.T) {
assert.Equal(t, []string{"externo 0", "interno 0"}, c.Errors.Errors())
}
func TestContextErrorWithJoinedErrors(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
assert.Empty(t, c.Errors)
firstErr := errors.New("first error")
secondErr := errors.New("second error")
c.Error(errors.Join(firstErr, secondErr)) //nolint: errcheck
assert.Len(t, c.Errors, 2)
assert.Equal(t, firstErr, c.Errors[0].Err)
assert.Equal(t, secondErr, c.Errors[1].Err)
assert.Equal(t, ErrorTypePrivate, c.Errors[0].Type)
assert.Equal(t, ErrorTypePrivate, c.Errors[1].Type)
}
func TestContextErrorWithNestedJoinedErrors(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Error(errors.Join( //nolint: errcheck
errors.New("first"),
errors.Join(errors.New("second"), errors.New("third")),
errors.New("fourth"),
))
assert.Len(t, c.Errors, 4)
assert.Equal(t, []string{"first", "second", "third", "fourth"}, c.Errors.Errors())
}
func TestContextErrorJoinPreservesGinErrorType(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
ginErr := &Error{Err: errors.New("typed"), Type: ErrorTypePublic}
c.Error(errors.Join(ginErr, errors.New("plain"))) //nolint: errcheck
assert.Len(t, c.Errors, 2)
assert.Equal(t, ErrorTypePublic, c.Errors[0].Type)
assert.Equal(t, ErrorTypePrivate, c.Errors[1].Type)
}
func TestContextErrorWithEmptyUnwrap(t *testing.T) {
// Custom error that implements Unwrap() []error but returns empty slice
c, _ := CreateTestContext(httptest.NewRecorder())
emptyJoinErr := emptyJoinError{errors.New("wrapped")}
c.Error(emptyJoinErr) //nolint: errcheck
assert.Len(t, c.Errors, 1)
assert.Equal(t, ErrorTypePrivate, c.Errors[0].Type)
}
type emptyJoinError struct{ err error }
func (e emptyJoinError) Error() string { return e.err.Error() }
func (e emptyJoinError) Unwrap() []error { return nil }
func TestContextAbortWithError(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)