11// Provide HTTP middleware functionality to validate that incoming requests conform to a given OpenAPI 3.x specification.
22//
3- // This provides middleware for an echo/v4 HTTP server.
3+ // This provides middleware for an echo/v5 HTTP server.
44//
55// 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.
66//
@@ -13,16 +13,15 @@ import (
1313 "fmt"
1414 "log"
1515 "net/http"
16- "net/url"
1716 "os"
18- "strings"
1917
2018 "github.com/getkin/kin-openapi/openapi3"
2119 "github.com/getkin/kin-openapi/openapi3filter"
2220 "github.com/getkin/kin-openapi/routers"
2321 "github.com/getkin/kin-openapi/routers/gorillamux"
2422 "github.com/labstack/echo/v5"
2523 echomiddleware "github.com/labstack/echo/v5/middleware"
24+ "github.com/oapi-codegen/echo-middleware/internal/validation"
2625)
2726
2827const (
@@ -126,21 +125,13 @@ func OapiRequestValidatorWithOptions(spec *openapi3.T, options *Options) echo.Mi
126125 }
127126}
128127
129- // ValidateRequestFromContext is called from the middleware above and actually does the work
130- // of validating a request.
131- func ValidateRequestFromContext (ctx * echo.Context , router routers.Router , options * Options ) * echo.HTTPError {
132- req := ctx .Request ()
128+ // ValidateRequestFromContext validates an incoming request using the OpenAPI spec and returns an error if validation fails.
129+ // It is called from the middleware and does the actual work of validating a request.
130+ func ValidateRequestFromContext (c * echo.Context , router routers.Router , options * Options ) * echo.HTTPError {
131+ req := c .Request ()
133132
134- if options != nil && options .Prefix != "" {
135- // Clone the request so downstream handlers still see the original path.
136- clone := req .Clone (req .Context ())
137- clone .URL .Path = strings .TrimPrefix (clone .URL .Path , options .Prefix )
138- req = clone
139- }
140-
141- route , pathParams , err := router .FindRoute (req )
142-
143- // We failed to find a matching route for the request.
133+ // Find the matching route
134+ route , pathParams , err := validation .FindRoute (req , router , getPrefix (options ))
144135 if err != nil {
145136 if errors .Is (err , routers .ErrMethodNotAllowed ) {
146137 return echo .NewHTTPError (http .StatusMethodNotAllowed , "" )
@@ -152,58 +143,33 @@ func ValidateRequestFromContext(ctx *echo.Context, router routers.Router, option
152143 // either server, or path, or something.
153144 return echo .NewHTTPError (http .StatusNotFound , e .Reason )
154145 default :
155- // This should never happen today, but if our upstream code changes ,
156- // we don't want to crash the server, so handle the unexpected error.
146+ // If our upstream code changes, we don't want to crash the server ,
147+ // so handle the unexpected error.
157148 return echo .NewHTTPError (http .StatusInternalServerError ,
158149 fmt .Sprintf ("error validating route: %s" , err .Error ()))
159150 }
160151 }
161152
162- // gorillamux uses UseEncodedPath(), so path parameters are returned in
163- // their percent-encoded form. Unescape them before passing to
164- // openapi3filter, which expects decoded values.
165- for k , v := range pathParams {
166- if unescaped , err := url .PathUnescape (v ); err == nil {
167- pathParams [k ] = unescaped
168- }
169- }
170-
171- validationInput := & openapi3filter.RequestValidationInput {
172- Request : req ,
173- PathParams : pathParams ,
174- Route : route ,
153+ // Build validation context with Echo context and user data
154+ requestContext := context .WithValue (context .Background (), validation .EchoContextKey , c ) //nolint:staticcheck
155+ if options != nil && options .UserData != nil {
156+ requestContext = context .WithValue (requestContext , validation .UserDataKey , options .UserData ) //nolint:staticcheck
175157 }
176158
177- // Pass the Echo context into the request validator, so that any callbacks
178- // which it invokes make it available.
179- requestContext := context .WithValue (context .Background (), EchoContextKey , ctx ) //nolint:staticcheck
180-
181- if options != nil {
182- validationInput .Options = & options .Options
183- validationInput .ParamDecoder = options .ParamDecoder
184- requestContext = context .WithValue (requestContext , UserDataKey , options .UserData ) //nolint:staticcheck
185- }
186-
187- err = openapi3filter .ValidateRequest (requestContext , validationInput )
188- if err != nil {
189- me := openapi3.MultiError {}
190- if errors .As (err , & me ) {
191- errFunc := getMultiErrorHandlerFromOptions (options )
192- return errFunc (me )
159+ // Perform OpenAPI validation
160+ validationErr := validation .ValidateRequest (requestContext , req , route , pathParams , getFilterOptions (options ), getParamDecoder (options ))
161+ if validationErr != nil {
162+ if validationErr .IsMultiError {
163+ multiErr := validationErr .MultiErrors
164+ if options != nil && options .MultiErrorHandler != nil {
165+ return options .MultiErrorHandler (multiErr )
166+ }
167+ return defaultMultiErrorHandler (multiErr )
193168 }
194169
195- switch e := err .(type ) {
196- case * openapi3filter.RequestError :
197- // We've got a bad request
198- // Split up the verbose error by lines and return the first one
199- // openapi errors seem to be multi-line with a decent message on the first
200- errorLines := strings .Split (e .Error (), "\n " )
201- return & echo.HTTPError {
202- Code : http .StatusBadRequest ,
203- Message : errorLines [0 ],
204- }
205- case * openapi3filter.SecurityRequirementsError :
206- for _ , err := range e .Errors {
170+ // Handle SecurityRequirementsError by extracting HTTPError if present
171+ if validationErr .IsSecurityError {
172+ for _ , err := range validationErr .SecurityErrors {
207173 var httpErr * echo.HTTPError
208174 if errors .As (err , & httpErr ) {
209175 return httpErr
@@ -213,19 +179,14 @@ func ValidateRequestFromContext(ctx *echo.Context, router routers.Router, option
213179 return echo .NewHTTPError (coder .StatusCode (), err .Error ())
214180 }
215181 }
216- return & echo.HTTPError {
217- Code : http .StatusForbidden ,
218- Message : e .Error (),
219- }
220- default :
221- // This should never happen today, but if our upstream code changes,
222- // we don't want to crash the server, so handle the unexpected error.
223- return & echo.HTTPError {
224- Code : http .StatusInternalServerError ,
225- Message : fmt .Sprintf ("error validating request: %s" , err ),
226- }
182+ }
183+
184+ return & echo.HTTPError {
185+ Code : validationErr .StatusCode ,
186+ Message : validationErr .Message ,
227187 }
228188 }
189+
229190 return nil
230191}
231192
@@ -260,26 +221,36 @@ func getSkipperFromOptions(options *Options) echomiddleware.Skipper {
260221 return options .Skipper
261222}
262223
263- // attempt to get the MultiErrorHandler from the options. If it is not set,
264- // return a default handler
265- func getMultiErrorHandlerFromOptions (options * Options ) MultiErrorHandler {
266- if options == nil {
267- return defaultMultiErrorHandler
224+ // defaultMultiErrorHandler returns a StatusBadRequest (400) and a list
225+ // of all of the errors. This method is called if there are no other
226+ // methods defined on the options.
227+ func defaultMultiErrorHandler (me openapi3.MultiError ) * echo.HTTPError {
228+ return & echo.HTTPError {
229+ Code : http .StatusBadRequest ,
230+ Message : me .Error (),
268231 }
232+ }
269233
270- if options .MultiErrorHandler == nil {
271- return defaultMultiErrorHandler
234+ // getPrefix gets the prefix from options if set
235+ func getPrefix (options * Options ) string {
236+ if options == nil {
237+ return ""
272238 }
239+ return options .Prefix
240+ }
273241
274- return options .MultiErrorHandler
242+ // getFilterOptions gets the openapi3filter.Options from options if set
243+ func getFilterOptions (options * Options ) * openapi3filter.Options {
244+ if options == nil {
245+ return nil
246+ }
247+ return & options .Options
275248}
276249
277- // defaultMultiErrorHandler returns a StatusBadRequest (400) and a list
278- // of all of the errors. This method is called if there are no other
279- // methods defined on the options.
280- func defaultMultiErrorHandler (me openapi3.MultiError ) * echo.HTTPError {
281- return & echo.HTTPError {
282- Code : http .StatusBadRequest ,
283- Message : me .Error (),
284- }
250+ // getParamDecoder gets the ParamDecoder from options if set
251+ func getParamDecoder (options * Options ) openapi3filter.ContentParameterDecoder {
252+ if options == nil {
253+ return nil
254+ }
255+ return options .ParamDecoder
285256}
0 commit comments