Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ generate:
go generate ./...

test:
go test -cover ./...
echo "=== ROOT MODULE (Echo v4) ===" && go test -cover ./... -v && echo "" && echo "=== ECHOV5 SUBMODULE (Echo v5) ===" && cd echov5 && go test -cover ./... -v .

tidy:
@echo "tidy..."
Expand Down
29 changes: 29 additions & 0 deletions echov5/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module github.com/oapi-codegen/echo-middleware/echov5

go 1.25.0

require (
github.com/getkin/kin-openapi v0.135.0
github.com/labstack/echo/v5 v5.1.0
github.com/oapi-codegen/echo-middleware v0.0.0-00010101000000-000000000000
github.com/stretchr/testify v1.11.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
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/mailru/easyjson v0.7.7 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/oasdiff/yaml v0.0.9 // indirect
github.com/oasdiff/yaml3 v0.0.9 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/woodsbury/decimal128 v1.3.0 // indirect
golang.org/x/time v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/oapi-codegen/echo-middleware => ../
51 changes: 51 additions & 0 deletions echov5/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getkin/kin-openapi v0.135.0 h1:751SjYfbiwqukYuVjwYEIKNfrSwS5YpA7DZnKSwQgtg=
github.com/getkin/kin-openapi v0.135.0/go.mod h1:6dd5FJl6RdX4usBtFBaQhk9q62Yb2J0Mk5IhUO/QqFI=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
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/v5 v5.1.0 h1:MvIRydoN+p9cx/zq8Lff6YXqUW2ZaEsOMISzEGSMrBI=
github.com/labstack/echo/v5 v5.1.0/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/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.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48=
github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM=
github.com/oasdiff/yaml3 v0.0.9 h1:rWPrKccrdUm8J0F3sGuU+fuh9+1K/RdJlWF7O/9yw2g=
github.com/oasdiff/yaml3 v0.0.9/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
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/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
256 changes: 256 additions & 0 deletions echov5/oapi_validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
// Provide HTTP middleware functionality to validate that incoming requests conform to a given OpenAPI 3.x specification.
//
// 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.
//
// This is _intended_ to be used with code that's generated through https://pkg.go.dev/github.com/oapi-codegen/oapi-codegen, but should work otherwise.
package echomiddleware

import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"

"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
"github.com/getkin/kin-openapi/routers"
"github.com/getkin/kin-openapi/routers/gorillamux"
"github.com/labstack/echo/v5"
echomiddleware "github.com/labstack/echo/v5/middleware"
"github.com/oapi-codegen/echo-middleware/internal/validation"
)

const (
EchoContextKey = "oapi-codegen/echo-context"
UserDataKey = "oapi-codegen/user-data"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these two are the same as the ones in internal/validation/validate.go

could/should you use those directly?


// OapiValidatorFromYamlFile is an Echo middleware function which validates incoming HTTP requests
// to make sure that they conform to the given OAPI 3.0 specification. When
// OAPI validation fails on the request, we return an HTTP/400.
// Create validator middleware from a YAML file path
func OapiValidatorFromYamlFile(path string) (echo.MiddlewareFunc, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("error reading %s: %w", path, err)
}

spec, err := openapi3.NewLoader().LoadFromData(data)
if err != nil {
return nil, fmt.Errorf("error parsing %s as OpenAPI YAML: %w", path, err)
}
return OapiRequestValidator(spec), nil
}

// OapiRequestValidator Creates the middleware to validate that incoming requests match the given OpenAPI 3.x spec, with a default set of configuration.
func OapiRequestValidator(spec *openapi3.T) echo.MiddlewareFunc {
return OapiRequestValidatorWithOptions(spec, nil)
}

// ErrorHandler is called when there is an error in validation
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

// Options to customize request validation. These are passed through to
// openapi3filter.
type Options struct {
// ErrorHandler is called when a validation error occurs.
//
// If not provided, `http.Error` will be called
ErrorHandler ErrorHandler
// Options contains any configuration for the underlying `openapi3filter`
Options openapi3filter.Options
// ParamDecoder is the openapi3filter.ContentParameterDecoder to be used for the decoding of the request body
//
// If unset, a default will be used
ParamDecoder openapi3filter.ContentParameterDecoder
// UserData is any user-specified data to inject into the context.Context, which is then passed in to the validation function.
//
// Set on the Context with the key `UserDataKey`.
UserData any
// Skipper an echo Skipper to allow skipping the middleware.
Skipper echomiddleware.Skipper
// MultiErrorHandler is called when there is an openapi3.MultiError (https://pkg.go.dev/github.com/getkin/kin-openapi/openapi3#MultiError) returned by the `openapi3filter`.
//
// If not provided `defaultMultiErrorHandler` will be used.
MultiErrorHandler MultiErrorHandler
// SilenceServersWarning allows silencing a warning for https://github.com/oapi-codegen/oapi-codegen/issues/882 that reports when an OpenAPI spec has `spec.Servers != nil`
SilenceServersWarning bool
// DoNotValidateServers ensures that there is no Host validation performed (see `SilenceServersWarning` and https://github.com/deepmap/oapi-codegen/issues/882 for more details)
DoNotValidateServers bool
// Prefix is stripped from the request path before validation. This is useful when the API is mounted under a sub-path
// (e.g. "/api") that isn't part of the OpenAPI spec's paths. The prefix must start with "/" if set.
Prefix string
}

// OapiRequestValidatorWithOptions Creates the middleware to validate that incoming requests match the given OpenAPI 3.x spec, allowing explicit configuration.
//
// NOTE that this may panic if the OpenAPI spec isn't valid, or if it cannot be used to create the middleware
func OapiRequestValidatorWithOptions(spec *openapi3.T, options *Options) echo.MiddlewareFunc {
if options != nil && options.DoNotValidateServers {
spec.Servers = nil
}

if spec.Servers != nil && (options == nil || !options.SilenceServersWarning) {
log.Println("WARN: OapiRequestValidatorWithOptions called with an OpenAPI spec that has `Servers` set. This may lead to an HTTP 400 with `no matching operation was found` when sending a valid request, as the validator performs `Host` header validation. If you're expecting `Host` header validation, you can silence this warning by setting `Options.SilenceServersWarning = true`. See https://github.com/oapi-codegen/oapi-codegen/issues/882 for more information.")
}

router, err := gorillamux.NewRouter(spec)
if err != nil {
panic(err)
}

skipper := getSkipperFromOptions(options)
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
if skipper(c) {
return next(c)
}

err := ValidateRequestFromContext(c, router, options)
if err != nil {
if options != nil && options.ErrorHandler != nil {
return options.ErrorHandler(c, err)
}
return err
}
return next(c)
}
}
}

// ValidateRequestFromContext validates an incoming request using the OpenAPI spec and returns an error if validation fails.
// It is called from the middleware and does the actual work of validating a request.
func ValidateRequestFromContext(c *echo.Context, router routers.Router, options *Options) *echo.HTTPError {
req := c.Request()

// Find the matching route
route, pathParams, err := validation.FindRoute(req, router, getPrefix(options))
if err != nil {
if errors.Is(err, routers.ErrMethodNotAllowed) {
return echo.NewHTTPError(http.StatusMethodNotAllowed, "")
}

switch e := err.(type) {
case *routers.RouteError:
// We've got a bad request, the path requested doesn't match
// either server, or path, or something.
return echo.NewHTTPError(http.StatusNotFound, e.Reason)
default:
// If our upstream code changes, we don't want to crash the server,
// so handle the unexpected error.
return echo.NewHTTPError(http.StatusInternalServerError,
fmt.Sprintf("error validating route: %s", err.Error()))
}
}

// Build validation context with Echo context and user data
requestContext := context.WithValue(context.Background(), validation.EchoContextKey, c) //nolint:staticcheck
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is context.Background() correct here, or should it be c.Request().Context() ?

if options != nil && options.UserData != nil {
requestContext = context.WithValue(requestContext, validation.UserDataKey, options.UserData) //nolint:staticcheck
}

// Perform OpenAPI validation
validationErr := validation.ValidateRequest(requestContext, req, route, pathParams, getFilterOptions(options), getParamDecoder(options))
if validationErr != nil {
if validationErr.IsMultiError {
multiErr := validationErr.MultiErrors
if options != nil && options.MultiErrorHandler != nil {
return options.MultiErrorHandler(multiErr)
}
return defaultMultiErrorHandler(multiErr)
}

// Handle SecurityRequirementsError by extracting HTTPError if present
if validationErr.IsSecurityError {
for _, err := range validationErr.SecurityErrors {
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())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should there be a fallback for errors that don't happen to be either echo.HTTPError or coder?

}
}

return &echo.HTTPError{
Code: validationErr.StatusCode,
Message: validationErr.Message,
}
}

return nil
}

// GetEchoContext gets the echo context from within requests. It returns
// nil if not found or wrong type.
func GetEchoContext(c context.Context) *echo.Context {
iface := c.Value(EchoContextKey)
if iface == nil {
return nil
}
eCtx, ok := iface.(*echo.Context)
if !ok {
return nil
}
return eCtx
}

func GetUserData(c context.Context) any {
return c.Value(UserDataKey)
}

// attempt to get the skipper from the options whether it is set or not
func getSkipperFromOptions(options *Options) echomiddleware.Skipper {
if options == nil {
return echomiddleware.DefaultSkipper
}

if options.Skipper == nil {
return echomiddleware.DefaultSkipper
}

return options.Skipper
}

// defaultMultiErrorHandler returns a StatusBadRequest (400) and a list
// of all of the errors. This method is called if there are no other
// methods defined on the options.
func defaultMultiErrorHandler(me openapi3.MultiError) *echo.HTTPError {
return &echo.HTTPError{
Code: http.StatusBadRequest,
Message: me.Error(),
}
}

// getPrefix gets the prefix from options if set
func getPrefix(options *Options) string {
if options == nil {
return ""
}
return options.Prefix
}

// getFilterOptions gets the openapi3filter.Options from options if set
func getFilterOptions(options *Options) *openapi3filter.Options {
if options == nil {
return nil
}
return &options.Options
}

// getParamDecoder gets the ParamDecoder from options if set
func getParamDecoder(options *Options) openapi3filter.ContentParameterDecoder {
if options == nil {
return nil
}
return options.ParamDecoder
}
Loading
Loading