Compare commits

...

3 Commits

Author SHA1 Message Date
lark
76dbc1c234
Merge e26253477bab8892477222039cf96b21cd9059fd into d75fcd4c9ab260e5225de590f1f0f8c0e0e12d11 2026-06-06 08:59:41 +08:00
willlv
e26253477b fix: resolve errcheck and gofmt lint errors
- Add '_ =' to ignore return values of c.Error() calls in tests
- Add '_ =' to recursive c.Error() call in Context.Error()
- Run gofmt to fix formatting issues
2026-03-23 16:19:12 +08:00
willlv
010dea5bd5 fix: unwrap errors.Join() created joined errors in Context.Error() (#4237)
Automatically unwrap errors created by errors.Join() when passed to
Context.Error(), so they are added as separate error entries instead
of a single combined error. This improves error log readability and
structure.

Before:
  Error #01: gin error
  Error #02: service error
  store error
  Error #03: other error

After:
  Error #01: gin error
  Error #02: service error
  Error #03: store error
  Error #04: other error

Closes #4237
2026-03-23 16:00:36 +08:00
2 changed files with 67 additions and 0 deletions

View File

@ -254,6 +254,22 @@ func (c *Context) Error(err error) *Error {
panic("err is nil")
}
// Unwrap errors.Join() created joinErr
type unwrapper interface {
Unwrap() []error
}
if joinErr, ok := err.(unwrapper); ok {
errs := joinErr.Unwrap()
if len(errs) > 0 {
// Recursively add each error from the joined errors
for _, e := range errs {
_ = c.Error(e)
}
// Return the last added error
return c.Errors.Last()
}
}
var parsedError *Error
ok := errors.As(err, &parsedError)
if !ok {

View File

@ -7,6 +7,7 @@ package gin
import (
"errors"
"fmt"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin/codec/json"
@ -138,3 +139,53 @@ func TestErrorUnwrap(t *testing.T) {
var testErrNonPointer TestErr
require.ErrorAs(t, wrappedErr, &testErrNonPointer)
}
// TestErrorJoinUnwrap tests that gin.Error() automatically unwraps errors.Join() created joined errors.
func TestErrorJoinUnwrap(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
// Test with errors.Join()
err1 := errors.New("service error")
err2 := errors.New("store error")
joinedErr := errors.Join(err1, err2)
_ = c.Error(joinedErr)
// Should be unwrapped into 2 separate errors
assert.Len(t, c.Errors, 2)
assert.Equal(t, "service error", c.Errors[0].Error())
assert.Equal(t, "store error", c.Errors[1].Error())
// Test mixed usage
c2, _ := CreateTestContext(httptest.NewRecorder())
_ = c2.Error(errors.New("gin error"))
_ = c2.Error(errors.Join(err1, err2))
_ = c2.Error(errors.New("other error"))
assert.Len(t, c2.Errors, 4)
expected := `Error #01: gin error
Error #02: service error
Error #03: store error
Error #04: other error
`
assert.Equal(t, expected, c2.Errors.String())
// Test empty join (edge case)
c3, _ := CreateTestContext(httptest.NewRecorder())
emptyJoin := errors.Join() // Creates nil error
if emptyJoin != nil {
_ = c3.Error(emptyJoin)
// errors.Join() with no arguments returns nil, so this shouldn't panic
}
// Test nested joins
c4, _ := CreateTestContext(httptest.NewRecorder())
err3 := errors.New("nested1")
err4 := errors.New("nested2")
nestedJoin := errors.Join(errors.Join(err3, err4), errors.New("outer"))
_ = c4.Error(nestedJoin)
assert.Len(t, c4.Errors, 3)
assert.Equal(t, "nested1", c4.Errors[0].Error())
assert.Equal(t, "nested2", c4.Errors[1].Error())
assert.Equal(t, "outer", c4.Errors[2].Error())
}