mirror of
https://github.com/gin-gonic/gin.git
synced 2026-06-05 10:28:21 +08:00
feat(context): unwrap joined errors in Context.Error()
This commit is contained in:
parent
d3ffc99852
commit
eb225210b8
21
context.go
21
context.go
@ -244,16 +244,37 @@ func (c *Context) AbortWithError(code int, err error) *Error {
|
|||||||
/********* ERROR MANAGEMENT *********/
|
/********* ERROR MANAGEMENT *********/
|
||||||
/************************************/
|
/************************************/
|
||||||
|
|
||||||
|
// joinedError is an interface for errors that wrap multiple inner errors,
|
||||||
|
// such as those created by errors.Join.
|
||||||
|
type joinedError interface {
|
||||||
|
Unwrap() []error
|
||||||
|
}
|
||||||
|
|
||||||
// Error attaches an error to the current context. The error is pushed to a list of errors.
|
// Error attaches an error to the current context. The error is pushed to a list of errors.
|
||||||
// It's a good idea to call Error for each error that occurred during the resolution of a request.
|
// It's a good idea to call Error for each error that occurred during the resolution of a request.
|
||||||
// A middleware can be used to collect all the errors and push them to a database together,
|
// A middleware can be used to collect all the errors and push them to a database together,
|
||||||
// print a log, or append it in the HTTP response.
|
// print a log, or append it in the HTTP response.
|
||||||
// Error will panic if err is nil.
|
// Error will panic if err is nil.
|
||||||
|
// If err is a joined error (created by errors.Join), it is unwrapped and each
|
||||||
|
// individual error is added as a separate entry. The last *Error added is returned.
|
||||||
func (c *Context) Error(err error) *Error {
|
func (c *Context) Error(err error) *Error {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
panic("err is nil")
|
panic("err is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unwrap joined errors so each one becomes a separate entry.
|
||||||
|
if joined, ok := err.(joinedError); ok {
|
||||||
|
errs := joined.Unwrap()
|
||||||
|
if len(errs) > 0 {
|
||||||
|
var last *Error
|
||||||
|
for _, e := range errs {
|
||||||
|
last = c.Error(e)
|
||||||
|
}
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
// Fall through for empty joined errors — store as-is.
|
||||||
|
}
|
||||||
|
|
||||||
var parsedError *Error
|
var parsedError *Error
|
||||||
ok := errors.As(err, &parsedError)
|
ok := errors.As(err, &parsedError)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@ -1960,6 +1960,63 @@ func TestContextTypedError(t *testing.T) {
|
|||||||
assert.Equal(t, []string{"externo 0", "interno 0"}, c.Errors.Errors())
|
assert.Equal(t, []string{"externo 0", "interno 0"}, c.Errors.Errors())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextErrorJoined(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
|
||||||
|
// Single errors around the joined one.
|
||||||
|
c.Error(errors.New("first")) //nolint: errcheck
|
||||||
|
|
||||||
|
joined := errors.Join(errors.New("service error"), errors.New("store error"))
|
||||||
|
last := c.Error(joined) //nolint: errcheck
|
||||||
|
|
||||||
|
c.Error(errors.New("last")) //nolint: errcheck
|
||||||
|
|
||||||
|
// Joined error must be unwrapped into separate entries.
|
||||||
|
assert.Len(t, c.Errors, 4)
|
||||||
|
assert.Equal(t, "first", c.Errors[0].Error())
|
||||||
|
assert.Equal(t, "service error", c.Errors[1].Error())
|
||||||
|
assert.Equal(t, "store error", c.Errors[2].Error())
|
||||||
|
assert.Equal(t, "last", c.Errors[3].Error())
|
||||||
|
|
||||||
|
// Return value is the last unwrapped entry.
|
||||||
|
assert.Equal(t, "store error", last.Error())
|
||||||
|
|
||||||
|
// All unwrapped entries default to ErrorTypePrivate.
|
||||||
|
for _, e := range c.Errors {
|
||||||
|
assert.Equal(t, ErrorTypePrivate, e.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String output should list each error individually.
|
||||||
|
expected := "Error #01: first\nError #02: service error\nError #03: store error\nError #04: last\n"
|
||||||
|
assert.Equal(t, expected, c.Errors.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextErrorNestedJoined(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
|
||||||
|
inner := errors.Join(errors.New("a"), errors.New("b"))
|
||||||
|
outer := errors.Join(inner, errors.New("c"))
|
||||||
|
c.Error(outer) //nolint: errcheck
|
||||||
|
|
||||||
|
assert.Len(t, c.Errors, 3)
|
||||||
|
assert.Equal(t, []string{"a", "b", "c"}, c.Errors.Errors())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextErrorJoinedWithTypedError(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
|
||||||
|
typedErr := &Error{Err: errors.New("typed"), Type: ErrorTypePublic, Meta: "meta"}
|
||||||
|
joined := errors.Join(errors.New("plain"), typedErr)
|
||||||
|
c.Error(joined) //nolint: errcheck
|
||||||
|
|
||||||
|
assert.Len(t, c.Errors, 2)
|
||||||
|
assert.Equal(t, "plain", c.Errors[0].Error())
|
||||||
|
assert.Equal(t, ErrorTypePrivate, c.Errors[0].Type)
|
||||||
|
assert.Equal(t, "typed", c.Errors[1].Error())
|
||||||
|
assert.Equal(t, ErrorTypePublic, c.Errors[1].Type)
|
||||||
|
assert.Equal(t, "meta", c.Errors[1].Meta)
|
||||||
|
}
|
||||||
|
|
||||||
func TestContextAbortWithError(t *testing.T) {
|
func TestContextAbortWithError(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := CreateTestContext(w)
|
c, _ := CreateTestContext(w)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user