From 79a9285c786c1d091e0d40d901ce1e81c3ff8928 Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sat, 3 Oct 2026 01:13:25 +0900 Subject: [PATCH 1/2] feat: pass request context to context-aware validators --- context.go | 16 ++++++++++ context_test.go | 77 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/context.go b/context.go index 805f2add8..c91d4e2fc 100644 --- a/context.go +++ b/context.go @@ -5,6 +5,7 @@ package echo import ( "bytes" + stdContext "context" "encoding/xml" "errors" "fmt" @@ -460,12 +461,27 @@ func (c *Context) Bind(i any) error { return c.echo.Binder.Bind(c, i) } +type validatorCtx interface { + ValidateCtx(ctx stdContext.Context, i any) error +} + // Validate validates provided `i`. It is usually called after `Context#Bind()`. // Validator must be registered using `Echo#Validator`. +// If the registered Validator also implements ValidateCtx(context.Context, any) error, +// that method is called instead of Validate, with the current request's context. +// The validator must still implement Validate to satisfy the Validator interface. +// If there is no request, context.Background() is used. func (c *Context) Validate(i any) error { if c.echo.Validator == nil { return ErrValidatorNotRegistered } + if v, ok := c.echo.Validator.(validatorCtx); ok { + ctx := stdContext.Background() + if req := c.Request(); req != nil { + ctx = req.Context() + } + return v.ValidateCtx(ctx, i) + } return c.echo.Validator.Validate(i) } diff --git a/context_test.go b/context_test.go index 72898da69..89f2209f0 100644 --- a/context_test.go +++ b/context_test.go @@ -5,9 +5,11 @@ package echo import ( "bytes" + stdContext "context" "crypto/tls" "encoding/json" "encoding/xml" + "errors" "fmt" "io" "io/fs" @@ -1119,12 +1121,85 @@ func TestContext_Validate(t *testing.T) { e := New() c := e.NewContext(nil, nil) - assert.Error(t, c.Validate(struct{}{})) + assert.ErrorIs(t, c.Validate(struct{}{}), ErrValidatorNotRegistered) e.Validator = &validator{} assert.NoError(t, c.Validate(struct{}{})) } +type validationFunc func(any) error + +func (v validationFunc) Validate(i any) error { return v(i) } + +type contextValidator struct { + validationFunc + validateCtx func(stdContext.Context, any) error +} + +func (v contextValidator) ValidateCtx(ctx stdContext.Context, i any) error { + return v.validateCtx(ctx, i) +} + +func TestContext_Validate_legacyError(t *testing.T) { + e := New() + c := e.NewContext(nil, nil) + payload := &struct{ Name string }{Name: "Jon Snow"} + wantErr := errors.New("validation failed") + e.Validator = validationFunc(func(i any) error { + assert.Same(t, payload, i) + return wantErr + }) + assert.Same(t, wantErr, c.Validate(payload)) +} + +func TestContext_Validate_withContext(t *testing.T) { + ctx, cancel := stdContext.WithCancel(stdContext.Background()) + defer cancel() + e := New() + c := e.NewContext(nil, nil) + c.SetRequest(httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)) + payload := &struct{ Name string }{Name: "Jon Snow"} + wantErr := errors.New("validation failed") + calls := 0 + e.Validator = contextValidator{ + validationFunc: func(any) error { + t.Fatal("Validate must not be called when ValidateCtx is implemented") + return nil + }, + validateCtx: func(got stdContext.Context, i any) error { + calls++ + assert.Same(t, ctx, got) + assert.Same(t, payload, i) + return wantErr + }, + } + + assert.Same(t, wantErr, c.Validate(payload)) + assert.Equal(t, 1, calls) + + cancel() + assert.Same(t, wantErr, c.Validate(payload)) + assert.Equal(t, 2, calls) +} + +func TestContext_Validate_withoutRequest(t *testing.T) { + e := New() + c := e.NewContext(nil, nil) + var gotCtx stdContext.Context + e.Validator = contextValidator{ + validateCtx: func(ctx stdContext.Context, i any) error { + gotCtx = ctx + return nil + }, + } + + assert.NoError(t, c.Validate(struct{}{})) + if assert.NotNil(t, gotCtx) { + assert.NoError(t, gotCtx.Err()) + assert.Nil(t, gotCtx.Done()) + } +} + func TestContext_QueryString(t *testing.T) { e := New() From 755c804e73a4b5cff74d5cf274f3e0091bebefef Mon Sep 17 00:00:00 2001 From: Jihun Kim Date: Sun, 4 Oct 2026 22:20:56 +0900 Subject: [PATCH 2/2] feat: make context-aware validation explicit --- context.go | 14 +++++++++----- context_test.go | 36 +++++++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/context.go b/context.go index c91d4e2fc..e7fe7ffd8 100644 --- a/context.go +++ b/context.go @@ -467,14 +467,18 @@ type validatorCtx interface { // Validate validates provided `i`. It is usually called after `Context#Bind()`. // Validator must be registered using `Echo#Validator`. -// If the registered Validator also implements ValidateCtx(context.Context, any) error, -// that method is called instead of Validate, with the current request's context. -// The validator must still implement Validate to satisfy the Validator interface. -// If there is no request, context.Background() is used. func (c *Context) Validate(i any) error { if c.echo.Validator == nil { return ErrValidatorNotRegistered } + return c.echo.Validator.Validate(i) +} + +// ValidateCtx validates i using the current request's context when the registered +// Validator implements ValidateCtx(context.Context, any) error. Otherwise it calls Validate. +// The validator must still implement Validate to satisfy the Validator interface. +// If there is no request, context.Background() is used. +func (c *Context) ValidateCtx(i any) error { if v, ok := c.echo.Validator.(validatorCtx); ok { ctx := stdContext.Background() if req := c.Request(); req != nil { @@ -482,7 +486,7 @@ func (c *Context) Validate(i any) error { } return v.ValidateCtx(ctx, i) } - return c.echo.Validator.Validate(i) + return c.Validate(i) } // Render renders a template with data and sends a text/html response with status diff --git a/context_test.go b/context_test.go index 89f2209f0..722531f51 100644 --- a/context_test.go +++ b/context_test.go @@ -1122,9 +1122,11 @@ func TestContext_Validate(t *testing.T) { c := e.NewContext(nil, nil) assert.ErrorIs(t, c.Validate(struct{}{}), ErrValidatorNotRegistered) + assert.ErrorIs(t, c.ValidateCtx(struct{}{}), ErrValidatorNotRegistered) e.Validator = &validator{} assert.NoError(t, c.Validate(struct{}{})) + assert.NoError(t, c.ValidateCtx(struct{}{})) } type validationFunc func(any) error @@ -1150,9 +1152,33 @@ func TestContext_Validate_legacyError(t *testing.T) { return wantErr }) assert.Same(t, wantErr, c.Validate(payload)) + assert.Same(t, wantErr, c.ValidateCtx(payload)) } -func TestContext_Validate_withContext(t *testing.T) { +type validationWrapper struct { + contextValidator + err error +} + +func (v validationWrapper) Validate(any) error { return v.err } + +func TestContext_Validate_wrapper(t *testing.T) { + e := New() + c := e.NewContext(nil, nil) + wantErr := errors.New("wrapper validation failed") + e.Validator = validationWrapper{ + contextValidator: contextValidator{ + validateCtx: func(stdContext.Context, any) error { + t.Fatal("Validate must not call an embedded ValidateCtx method") + return nil + }, + }, + err: wantErr, + } + assert.Same(t, wantErr, c.Validate(struct{}{})) +} + +func TestContext_ValidateCtx_withContext(t *testing.T) { ctx, cancel := stdContext.WithCancel(stdContext.Background()) defer cancel() e := New() @@ -1174,15 +1200,15 @@ func TestContext_Validate_withContext(t *testing.T) { }, } - assert.Same(t, wantErr, c.Validate(payload)) + assert.Same(t, wantErr, c.ValidateCtx(payload)) assert.Equal(t, 1, calls) cancel() - assert.Same(t, wantErr, c.Validate(payload)) + assert.Same(t, wantErr, c.ValidateCtx(payload)) assert.Equal(t, 2, calls) } -func TestContext_Validate_withoutRequest(t *testing.T) { +func TestContext_ValidateCtx_withoutRequest(t *testing.T) { e := New() c := e.NewContext(nil, nil) var gotCtx stdContext.Context @@ -1193,7 +1219,7 @@ func TestContext_Validate_withoutRequest(t *testing.T) { }, } - assert.NoError(t, c.Validate(struct{}{})) + assert.NoError(t, c.ValidateCtx(struct{}{})) if assert.NotNil(t, gotCtx) { assert.NoError(t, gotCtx.Err()) assert.Nil(t, gotCtx.Done())