From eebad370fac18367063a7915c94d1ff7f74f7ea9 Mon Sep 17 00:00:00 2001 From: Marcin Romaszewicz Date: Thu, 9 Jul 2026 09:17:49 -0700 Subject: [PATCH] feat: convert middleware to echo v5 Port the request validation middleware from echo v4 to echo v5, based on the implementation contributed in oapi-codegen/echo-middleware#48: - `echo.Context` (interface) becomes `*echo.Context` (struct pointer) throughout the API, including `ErrorHandler` and `GetEchoContext` - echo v5's `HTTPError` no longer has an exported `Internal` field, so validation errors are no longer attached to the returned HTTP errors - `SecurityRequirementsError` unwrapping uses `errors.As` and falls back to the v5 `StatusCode() int` error convention - depend on `github.com/labstack/echo/v5` v5.2.1, requiring Go 1.25+ Remove the conversion-in-progress notice from the README, leaving only the note that no release has been tagged yet. Co-authored-by: jinuthankachan --- README.md | 1 - go.mod | 13 ++-------- go.sum | 27 +++++--------------- oapi_validate.go | 46 +++++++++++++++++------------------ oapi_validate_example_test.go | 10 ++++---- oapi_validate_test.go | 31 ++++++++++++----------- 6 files changed, 51 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 29ebee1..8056925 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ This is _intended_ to be used with code that's generated through [`oapi-codegen` > Using Echo v4? You want [`oapi-codegen/echo-middleware`](https://github.com/oapi-codegen/echo-middleware) instead. > [!WARNING] -> The conversion of this repository from Echo v4 to Echo v5 is in progress, and no release has been tagged yet. This notice will be removed with the first release. ⚠️ This README may be for the latest development version, which may contain unreleased changes. Please ensure you're looking at the README for the latest release version. diff --git a/go.mod b/go.mod index 734f1a8..0cc8292 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/oapi-codegen/echo-v5-middleware -go 1.24.4 +go 1.25.0 require ( github.com/getkin/kin-openapi v0.134.0 - github.com/labstack/echo/v4 v4.15.1 + github.com/labstack/echo/v5 v5.2.1 github.com/stretchr/testify v1.11.1 ) @@ -14,23 +14,14 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/labstack/gommon v0.4.2 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c // indirect github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/ugorji/go/codec v1.2.11 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.2.2 // indirect github.com/woodsbury/decimal128 v1.3.0 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 24a7e23..2c3db38 100644 --- a/go.sum +++ b/go.sum @@ -16,16 +16,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs= -github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= -github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= -github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/labstack/echo/v5 v5.2.1 h1:TzpIksY6zLMzV0T0ycYbvTEoj9w6o6AcL5twg182VTY= +github.com/labstack/echo/v5 v5.2.1/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c h1:7ACFcSaQsrWtrH4WHHfUqE1C+f8r2uv8KGaW0jTNjus= @@ -42,21 +36,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= -github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/oapi_validate.go b/oapi_validate.go index f48ad69..614065b 100644 --- a/oapi_validate.go +++ b/oapi_validate.go @@ -1,6 +1,6 @@ // Provide HTTP middleware functionality to validate that incoming requests conform to a given OpenAPI 3.x specification. // -// This provides middleware for an echo/v4 HTTP server. +// This provides middleware for an echo/v5 HTTP server. // // This package is a lightweight wrapper over https://pkg.go.dev/github.com/getkin/kin-openapi/openapi3filter from https://pkg.go.dev/github.com/getkin/kin-openapi. // @@ -21,8 +21,8 @@ import ( "github.com/getkin/kin-openapi/openapi3filter" "github.com/getkin/kin-openapi/routers" "github.com/getkin/kin-openapi/routers/gorillamux" - "github.com/labstack/echo/v4" - echomiddleware "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + echomiddleware "github.com/labstack/echo/v5/middleware" ) const ( @@ -53,7 +53,7 @@ func OapiRequestValidator(spec *openapi3.T) echo.MiddlewareFunc { } // ErrorHandler is called when there is an error in validation -type ErrorHandler func(c echo.Context, err *echo.HTTPError) error +type ErrorHandler func(c *echo.Context, err *echo.HTTPError) error // MultiErrorHandler is called when the OpenAPI filter returns an openapi3.MultiError (https://pkg.go.dev/github.com/getkin/kin-openapi/openapi3#MultiError) type MultiErrorHandler func(openapi3.MultiError) *echo.HTTPError @@ -109,7 +109,7 @@ func OapiRequestValidatorWithOptions(spec *openapi3.T, options *Options) echo.Mi skipper := getSkipperFromOptions(options) return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { + return func(c *echo.Context) error { if skipper(c) { return next(c) } @@ -128,7 +128,7 @@ func OapiRequestValidatorWithOptions(spec *openapi3.T, options *Options) echo.Mi // ValidateRequestFromContext is called from the middleware above and actually does the work // of validating a request. -func ValidateRequestFromContext(ctx echo.Context, router routers.Router, options *Options) *echo.HTTPError { +func ValidateRequestFromContext(ctx *echo.Context, router routers.Router, options *Options) *echo.HTTPError { req := ctx.Request() if options != nil && options.Prefix != "" { @@ -143,7 +143,7 @@ func ValidateRequestFromContext(ctx echo.Context, router routers.Router, options // We failed to find a matching route for the request. if err != nil { if errors.Is(err, routers.ErrMethodNotAllowed) { - return echo.NewHTTPError(http.StatusMethodNotAllowed) + return echo.NewHTTPError(http.StatusMethodNotAllowed, "") } switch e := err.(type) { @@ -199,29 +199,30 @@ func ValidateRequestFromContext(ctx echo.Context, router routers.Router, options // openapi errors seem to be multi-line with a decent message on the first errorLines := strings.Split(e.Error(), "\n") return &echo.HTTPError{ - Code: http.StatusBadRequest, - Message: errorLines[0], - Internal: err, + Code: http.StatusBadRequest, + Message: errorLines[0], } case *openapi3filter.SecurityRequirementsError: for _, err := range e.Errors { - httpErr, ok := err.(*echo.HTTPError) - if ok { + var httpErr *echo.HTTPError + if errors.As(err, &httpErr) { return httpErr } + var coder interface{ StatusCode() int } + if errors.As(err, &coder) { + return echo.NewHTTPError(coder.StatusCode(), err.Error()) + } } return &echo.HTTPError{ - Code: http.StatusForbidden, - Message: e.Error(), - Internal: err, + Code: http.StatusForbidden, + Message: e.Error(), } default: // This should never happen today, but if our upstream code changes, // we don't want to crash the server, so handle the unexpected error. return &echo.HTTPError{ - Code: http.StatusInternalServerError, - Message: fmt.Sprintf("error validating request: %s", err), - Internal: err, + Code: http.StatusInternalServerError, + Message: fmt.Sprintf("error validating request: %s", err), } } } @@ -230,12 +231,12 @@ func ValidateRequestFromContext(ctx echo.Context, router routers.Router, options // GetEchoContext gets the echo context from within requests. It returns // nil if not found or wrong type. -func GetEchoContext(c context.Context) echo.Context { +func GetEchoContext(c context.Context) *echo.Context { iface := c.Value(EchoContextKey) if iface == nil { return nil } - eCtx, ok := iface.(echo.Context) + eCtx, ok := iface.(*echo.Context) if !ok { return nil } @@ -278,8 +279,7 @@ func getMultiErrorHandlerFromOptions(options *Options) MultiErrorHandler { // methods defined on the options. func defaultMultiErrorHandler(me openapi3.MultiError) *echo.HTTPError { return &echo.HTTPError{ - Code: http.StatusBadRequest, - Message: me.Error(), - Internal: me, + Code: http.StatusBadRequest, + Message: me.Error(), } } diff --git a/oapi_validate_example_test.go b/oapi_validate_example_test.go index c706a1a..b9ff6a6 100644 --- a/oapi_validate_example_test.go +++ b/oapi_validate_example_test.go @@ -11,7 +11,7 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" - "github.com/labstack/echo/v4" + "github.com/labstack/echo/v5" middleware "github.com/oapi-codegen/echo-v5-middleware" ) @@ -79,13 +79,13 @@ components: spec.Servers = nil e := echo.New() - e.POST("/resource", func(c echo.Context) error { + e.POST("/resource", func(c *echo.Context) error { fmt.Printf("%s /resource was called\n", c.Request().Method) return c.NoContent(http.StatusNoContent) }) - e.GET("/protected_resource", func(c echo.Context) error { + e.GET("/protected_resource", func(c *echo.Context) error { // NOTE that we're setting up our `authenticationFunc` (below) to /never/ allow any requests in - so if we get a response from this endpoint, our `authenticationFunc` hasn't correctly worked return c.NoContent(http.StatusNoContent) }) @@ -261,7 +261,7 @@ paths: spec.Servers = nil e := echo.New() - e.POST("/resource", func(c echo.Context) error { + e.POST("/resource", func(c *echo.Context) error { fmt.Printf("%s /resource was called\n", c.Request().Method) return c.NoContent(http.StatusNoContent) @@ -272,7 +272,7 @@ paths: return fmt.Errorf("this check always fails - don't let anyone in!") } - errorHandlerFunc := func(c echo.Context, err *echo.HTTPError) error { + errorHandlerFunc := func(c *echo.Context, err *echo.HTTPError) error { fmt.Printf("ErrorHandler: An HTTP %d was returned by the middleware with error message: %s\n", err.Code, err.Message) return c.String(err.Code, "This was rewritten by the ErrorHandler") } diff --git a/oapi_validate_test.go b/oapi_validate_test.go index 14a8fed..dc729a7 100644 --- a/oapi_validate_test.go +++ b/oapi_validate_test.go @@ -14,8 +14,8 @@ import ( "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3filter" - "github.com/labstack/echo/v4" - echomiddleware "github.com/labstack/echo/v4/middleware" + "github.com/labstack/echo/v5" + echomiddleware "github.com/labstack/echo/v5/middleware" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -79,7 +79,7 @@ func TestOapiRequestValidator(t *testing.T) { // Set up an authenticator to check authenticated function. It will allow // access to "someScope", but disallow others. options := Options{ - ErrorHandler: func(c echo.Context, err *echo.HTTPError) error { + ErrorHandler: func(c *echo.Context, err *echo.HTTPError) error { return c.String(err.Code, "test: "+err.Error()) }, Options: openapi3filter.Options{ @@ -111,7 +111,7 @@ func TestOapiRequestValidator(t *testing.T) { // Install a request handler for /resource. We want to make sure it doesn't // get called. - e.GET("/resource", func(c echo.Context) error { + e.GET("/resource", func(c *echo.Context) error { called = true return nil }) @@ -155,7 +155,7 @@ func TestOapiRequestValidator(t *testing.T) { } // Add a handler for the POST message - e.POST("/resource", func(c echo.Context) error { + e.POST("/resource", func(c *echo.Context) error { called = true return c.NoContent(http.StatusNoContent) }) @@ -187,7 +187,7 @@ func TestOapiRequestValidator(t *testing.T) { called = false } - e.GET("/protected_resource", func(c echo.Context) error { + e.GET("/protected_resource", func(c *echo.Context) error { called = true return c.NoContent(http.StatusNoContent) @@ -201,7 +201,7 @@ func TestOapiRequestValidator(t *testing.T) { called = false } - e.GET("/protected_resource2", func(c echo.Context) error { + e.GET("/protected_resource2", func(c *echo.Context) error { called = true return c.NoContent(http.StatusNoContent) }) @@ -213,7 +213,7 @@ func TestOapiRequestValidator(t *testing.T) { called = false } - e.GET("/protected_resource_401", func(c echo.Context) error { + e.GET("/protected_resource_401", func(c *echo.Context) error { called = true return c.NoContent(http.StatusNoContent) }) @@ -252,7 +252,7 @@ func TestOapiRequestValidatorWithOptionsMultiError(t *testing.T) { // Install a request handler for /resource. We want to make sure it doesn't // get called. - e.GET("/multiparamresource", func(c echo.Context) error { + e.GET("/multiparamresource", func(c *echo.Context) error { called = true return nil }) @@ -346,9 +346,8 @@ func TestOapiRequestValidatorWithOptionsMultiErrorAndCustomHandler(t *testing.T) }, MultiErrorHandler: func(me openapi3.MultiError) *echo.HTTPError { return &echo.HTTPError{ - Code: http.StatusTeapot, - Message: me.Error(), - Internal: me, + Code: http.StatusTeapot, + Message: me.Error(), } }, } @@ -360,7 +359,7 @@ func TestOapiRequestValidatorWithOptionsMultiErrorAndCustomHandler(t *testing.T) // Install a request handler for /resource. We want to make sure it doesn't // get called. - e.GET("/multiparamresource", func(c echo.Context) error { + e.GET("/multiparamresource", func(c *echo.Context) error { called = true return nil }) @@ -454,7 +453,7 @@ func TestOapiRequestValidatorWithPrefix(t *testing.T) { called := false // Register handler under the prefixed path (as echo sees it) - e.GET("/api/resource", func(c echo.Context) error { + e.GET("/api/resource", func(c *echo.Context) error { called = true // The original request path should be preserved for the handler assert.Equal(t, "/api/resource", c.Request().URL.Path) @@ -501,11 +500,11 @@ func TestEncodedPathParams(t *testing.T) { // Register handlers for parameterized paths. These must be registered // before any requests are made so echo allocates enough param slots. - e.GET("/resource/maxlength/:param", func(c echo.Context) error { + e.GET("/resource/maxlength/:param", func(c *echo.Context) error { called = true return c.NoContent(http.StatusNoContent) }) - e.GET("/resource/pattern/:param", func(c echo.Context) error { + e.GET("/resource/pattern/:param", func(c *echo.Context) error { called = true return c.NoContent(http.StatusNoContent) })