Repository: deepmap/oapi-codegen (go)

Issue:
If `MultiError` is enabled via `chi_middleware.OapiRequestValidatorWithOptions()`, the sever always return 500 as status code about invalid request.
I'm using `chi` with `github.com/deepmap/oapi-codegen/pkg/chi-middleware` to validate the request body.
But if `MultiError` is enabled looks like below, it always return 500 as status code when the request is invalid. I expect it returns 400.
```go
	r := chi.NewRouter()
	r.Use(
		middleware.RequestID,
		middleware.Recoverer,
		chi_middleware.OapiRequestValidatorWithOptions(swagger, &chi_middleware.Options{
			Options: openapi3filter.Options{
				MultiError: true,
			},
		}),
	)
```

I think this switch statement don't care about `openapi3.MultiError` in `validateRequest()`
https://github.com/deepmap/oapi-codegen/blob/master/pkg/chi-middleware/oapi_validate.go#L75-L88
```go
	if err := openapi3filter.ValidateRequest(context.Background(), requestValidationInput); err != nil {
		switch e := err.(type) {
		case *openapi3filter.RequestError:
			// We've got a bad request
			// Split up the verbose error by lines and return the first one
			// openapi errors seem to be multi-line with a decent message on the first
			errorLines := strings.Split(e.Error(), "\n")
			return http.StatusBadRequest, fmt.Errorf(errorLines[0])
		case *openapi3filter.SecurityRequirementsError:
			return http.StatusUnauthorized, err
		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 http.StatusInternalServerError, fmt.Errorf("error validating route: %s", err.Error())
		}
	}
```

So, it always return `http.StatusInternalServerError`.

## Environment

- `opai-codegen`: v1.8.2
- Go: 1.17

Pull request description:
handle oapi3.MultiError messages
This adds support for handling the MultiError responses from the ValidateResponse method for the various middlewares. It returns the entire error message verbatim, which may not be the desired response, but I wanted to start somewhere. Open to alternate solutions for the return messages.

fixes #441 

Patch:
diff --git a/pkg/chi-middleware/oapi_validate.go b/pkg/chi-middleware/oapi_validate.go
index dfce720..e230239 100644
--- a/pkg/chi-middleware/oapi_validate.go
+++ b/pkg/chi-middleware/oapi_validate.go
@@ -5,6 +5,7 @@ package middleware
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net/http"
 	"strings"
@@ -18,10 +19,14 @@ import (
 // ErrorHandler is called when there is an error in validation
 type ErrorHandler func(w http.ResponseWriter, message string, statusCode int)
 
+// MultiErrorHandler is called when oapi returns a MultiError type
+type MultiErrorHandler func(openapi3.MultiError) (int, error)
+
 // Options to customize request validation, openapi3filter specified options will be passed through.
 type Options struct {
-	Options      openapi3filter.Options
-	ErrorHandler ErrorHandler
+	Options           openapi3filter.Options
+	ErrorHandler      ErrorHandler
+	MultiErrorHandler MultiErrorHandler
 }
 
 // OapiRequestValidator Creates middleware to validate request by swagger spec.
@@ -80,6 +85,12 @@ func validateRequest(r *http.Request, router routers.Router, options *Options) (
 	}
 
 	if err := openapi3filter.ValidateRequest(context.Background(), requestValidationInput); err != nil {
+		me := openapi3.MultiError{}
+		if errors.As(err, &me) {
+			errFunc := getMultiErrorHandlerFromOptions(options)
+			return errFunc(me)
+		}
+
 		switch e := err.(type) {
 		case *openapi3filter.RequestError:
 			// We've got a bad request
@@ -98,3 +109,24 @@ func validateRequest(r *http.Request, router routers.Router, options *Options) (
 
 	return http.StatusOK, nil
 }
+
+// attempt to get the MultiErrorHandler from the options. If it is not set,
+// return a default handler
+func getMultiErrorHandlerFromOptions(options *Options) MultiErrorHandler {
+	if options == nil {
+		return defaultMultiErrorHandler
+	}
+
+	if options.MultiErrorHandler == nil {
+		return defaultMultiErrorHandler
+	}
+
+	return options.MultiErrorHandler
+}
+
+// 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) (int, error) {
+	return http.StatusBadRequest, me
+}
diff --git a/pkg/gin-middleware/oapi_validate.go b/pkg/gin-middleware/oapi_validate.go
index 1f1687d..b165fe0 100644
--- a/pkg/gin-middleware/oapi_validate.go
+++ b/pkg/gin-middleware/oapi_validate.go
@@ -59,13 +59,17 @@ func OapiRequestValidator(swagger *openapi3.T) gin.HandlerFunc {
 // ErrorHandler is called when there is an error in validation
 type ErrorHandler func(c *gin.Context, message string, statusCode int)
 
+// MultiErrorHandler is called when oapi returns a MultiError type
+type MultiErrorHandler func(openapi3.MultiError) error
+
 // Options to customize request validation. These are passed through to
 // openapi3filter.
 type Options struct {
-	ErrorHandler ErrorHandler
-	Options      openapi3filter.Options
-	ParamDecoder openapi3filter.ContentParameterDecoder
-	UserData     interface{}
+	ErrorHandler      ErrorHandler
+	Options           openapi3filter.Options
+	ParamDecoder      openapi3filter.ContentParameterDecoder
+	UserData          interface{}
+	MultiErrorHandler MultiErrorHandler
 }
 
 // Create a validator from a swagger object, with validation options
@@ -128,6 +132,12 @@ func ValidateRequestFromContext(c *gin.Context, router routers.Router, options *
 
 	err = openapi3filter.ValidateRequest(requestContext, validationInput)
 	if err != nil {
+		me := openapi3.MultiError{}
+		if errors.As(err, &me) {
+			errFunc := getMultiErrorHandlerFromOptions(options)
+			return errFunc(me)
+		}
+
 		switch e := err.(type) {
 		case *openapi3filter.RequestError:
 			// We've got a bad request
@@ -163,3 +173,24 @@ func GetGinContext(c context.Context) *gin.Context {
 func GetUserData(c context.Context) interface{} {
 	return c.Value(UserDataKey)
 }
+
+// attempt to get the MultiErrorHandler from the options. If it is not set,
+// return a default handler
+func getMultiErrorHandlerFromOptions(options *Options) MultiErrorHandler {
+	if options == nil {
+		return defaultMultiErrorHandler
+	}
+
+	if options.MultiErrorHandler == nil {
+		return defaultMultiErrorHandler
+	}
+
+	return options.MultiErrorHandler
+}
+
+// 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) error {
+	return fmt.Errorf("multiple errors encountered: %s", me)
+}
diff --git a/pkg/middleware/oapi_validate.go b/pkg/middleware/oapi_validate.go
index b0bde5d..00ce1da 100644
--- a/pkg/middleware/oapi_validate.go
+++ b/pkg/middleware/oapi_validate.go
@@ -16,6 +16,7 @@ package middleware
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -61,14 +62,18 @@ func OapiRequestValidator(swagger *openapi3.T) echo.MiddlewareFunc {
 // ErrorHandler is called when there is an error in validation
 type ErrorHandler func(c echo.Context, err *echo.HTTPError) error
 
+// MultiErrorHandler is called when oapi returns a MultiError type
+type MultiErrorHandler func(openapi3.MultiError) *echo.HTTPError
+
 // Options to customize request validation. These are passed through to
 // openapi3filter.
 type Options struct {
-	ErrorHandler ErrorHandler
-	Options      openapi3filter.Options
-	ParamDecoder openapi3filter.ContentParameterDecoder
-	UserData     interface{}
-	Skipper      echomiddleware.Skipper
+	ErrorHandler      ErrorHandler
+	Options           openapi3filter.Options
+	ParamDecoder      openapi3filter.ContentParameterDecoder
+	UserData          interface{}
+	Skipper           echomiddleware.Skipper
+	MultiErrorHandler MultiErrorHandler
 }
 
 // OapiRequestValidatorWithOptions creates a validator from a swagger object, with validation options
@@ -136,6 +141,12 @@ func ValidateRequestFromContext(ctx echo.Context, router routers.Router, options
 
 	err = openapi3filter.ValidateRequest(requestContext, validationInput)
 	if err != nil {
+		me := openapi3.MultiError{}
+		if errors.As(err, &me) {
+			errFunc := getMultiErrorHandlerFromOptions(options)
+			return errFunc(me)
+		}
+
 		switch e := err.(type) {
 		case *openapi3filter.RequestError:
 			// We've got a bad request
@@ -202,3 +213,28 @@ func getSkipperFromOptions(options *Options) echomiddleware.Skipper {
 
 	return options.Skipper
 }
+
+// attempt to get the MultiErrorHandler from the options. If it is not set,
+// return a default handler
+func getMultiErrorHandlerFromOptions(options *Options) MultiErrorHandler {
+	if options == nil {
+		return defaultMultiErrorHandler
+	}
+
+	if options.MultiErrorHandler == nil {
+		return defaultMultiErrorHandler
+	}
+
+	return options.MultiErrorHandler
+}
+
+// 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(),
+		Internal: me,
+	}
+}


Test patch:
diff --git a/pkg/chi-middleware/oapi_validate_test.go b/pkg/chi-middleware/oapi_validate_test.go
index 8df1a2e..4d3c925 100644
--- a/pkg/chi-middleware/oapi_validate_test.go
+++ b/pkg/chi-middleware/oapi_validate_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	_ "embed"
 	"errors"
+	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -54,6 +55,207 @@ func TestOapiRequestValidator(t *testing.T) {
 	testRequestValidatorBasicFunctions(t, r)
 }
 
+func TestOapiRequestValidatorWithOptionsMultiError(t *testing.T) {
+	swagger, err := openapi3.NewLoader().LoadFromData([]byte(testSchema))
+	require.NoError(t, err, "Error initializing swagger")
+
+	r := chi.NewRouter()
+
+	// Set up an authenticator to check authenticated function. It will allow
+	// access to "someScope", but disallow others.
+	options := Options{
+		Options: openapi3filter.Options{
+			ExcludeRequestBody:    false,
+			ExcludeResponseBody:   false,
+			IncludeResponseStatus: true,
+			MultiError:            true,
+		},
+	}
+
+	// register middleware
+	r.Use(OapiRequestValidatorWithOptions(swagger, &options))
+
+	called := false
+
+	// Install a request handler for /resource. We want to make sure it doesn't
+	// get called.
+	r.Get("/multiparamresource", func(w http.ResponseWriter, r *http.Request) {
+		called = true
+	})
+
+	// Let's send a good request, it should pass
+	{
+		rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=50&id2=50")
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.True(t, called, "Handler should have been called")
+		called = false
+	}
+
+	// Let's send a request with a missing parameter, it should return
+	// a bad status
+	{
+		rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=50")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \"id2\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a 2 missing parameters, it should return
+	// a bad status
+	{
+		rec := doGet(t, r, "http://deepmap.ai/multiparamresource")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \"id\"")
+			assert.Contains(t, string(body), "value is required but missing")
+			assert.Contains(t, string(body), "parameter \"id2\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a 1 missing parameter, and another outside
+	// or the parameters. It should return a bad status
+	{
+		rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=500")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \"id\"")
+			assert.Contains(t, string(body), "number must be at most 100")
+			assert.Contains(t, string(body), "parameter \"id2\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a parameters that do not meet spec. It should
+	// return a bad status
+	{
+		rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=abc&id2=1")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \"id\"")
+			assert.Contains(t, string(body), "parsing \"abc\": invalid syntax")
+			assert.Contains(t, string(body), "parameter \"id2\"")
+			assert.Contains(t, string(body), "number must be at least 10")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+}
+
+func TestOapiRequestValidatorWithOptionsMultiErrorAndCustomHandler(t *testing.T) {
+	swagger, err := openapi3.NewLoader().LoadFromData([]byte(testSchema))
+	require.NoError(t, err, "Error initializing swagger")
+
+	r := chi.NewRouter()
+
+	// Set up an authenticator to check authenticated function. It will allow
+	// access to "someScope", but disallow others.
+	options := Options{
+		Options: openapi3filter.Options{
+			ExcludeRequestBody:    false,
+			ExcludeResponseBody:   false,
+			IncludeResponseStatus: true,
+			MultiError:            true,
+		},
+		MultiErrorHandler: func(me openapi3.MultiError) (int, error) {
+			return http.StatusTeapot, me
+		},
+	}
+
+	// register middleware
+	r.Use(OapiRequestValidatorWithOptions(swagger, &options))
+
+	called := false
+
+	// Install a request handler for /resource. We want to make sure it doesn't
+	// get called.
+	r.Get("/multiparamresource", func(w http.ResponseWriter, r *http.Request) {
+		called = true
+	})
+
+	// Let's send a good request, it should pass
+	{
+		rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=50&id2=50")
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.True(t, called, "Handler should have been called")
+		called = false
+	}
+
+	// Let's send a request with a missing parameter, it should return
+	// a bad status
+	{
+		rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=50")
+		assert.Equal(t, http.StatusTeapot, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \"id2\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a 2 missing parameters, it should return
+	// a bad status
+	{
+		rec := doGet(t, r, "http://deepmap.ai/multiparamresource")
+		assert.Equal(t, http.StatusTeapot, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \"id\"")
+			assert.Contains(t, string(body), "value is required but missing")
+			assert.Contains(t, string(body), "parameter \"id2\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a 1 missing parameter, and another outside
+	// or the parameters. It should return a bad status
+	{
+		rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=500")
+		assert.Equal(t, http.StatusTeapot, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \"id\"")
+			assert.Contains(t, string(body), "number must be at most 100")
+			assert.Contains(t, string(body), "parameter \"id2\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a parameters that do not meet spec. It should
+	// return a bad status
+	{
+		rec := doGet(t, r, "http://deepmap.ai/multiparamresource?id=abc&id2=1")
+		assert.Equal(t, http.StatusTeapot, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \"id\"")
+			assert.Contains(t, string(body), "parsing \"abc\": invalid syntax")
+			assert.Contains(t, string(body), "parameter \"id2\"")
+			assert.Contains(t, string(body), "number must be at least 10")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+}
+
 func TestOapiRequestValidatorWithOptions(t *testing.T) {
 	swagger, err := openapi3.NewLoader().LoadFromData([]byte(testSchema))
 	require.NoError(t, err, "Error initializing swagger")
diff --git a/pkg/chi-middleware/test_spec.yaml b/pkg/chi-middleware/test_spec.yaml
index f2ce175..3a7b169 100644
--- a/pkg/chi-middleware/test_spec.yaml
+++ b/pkg/chi-middleware/test_spec.yaml
@@ -66,6 +66,35 @@ paths:
       responses:
         '401':
           description: no content
+  /multiparamresource:
+    get:
+      operationId: getResource
+      parameters:
+        - name: id
+          in: query
+          required: true
+          schema:
+            type: integer
+            minimum: 10
+            maximum: 100
+        - name: id2
+          required: true
+          in: query
+          schema:
+            type: integer
+            minimum: 10
+            maximum: 100
+      responses:
+        '200':
+            description: success
+            content:
+              application/json:
+                schema:
+                  properties:
+                    name:
+                      type: string
+                    id:
+                      type: integer
 components:
   securitySchemes:
     BearerAuth:
diff --git a/pkg/gin-middleware/oapi_validate_test.go b/pkg/gin-middleware/oapi_validate_test.go
index 7373071..2d0f1d3 100644
--- a/pkg/gin-middleware/oapi_validate_test.go
+++ b/pkg/gin-middleware/oapi_validate_test.go
@@ -18,6 +18,8 @@ import (
 	"context"
 	_ "embed"
 	"errors"
+	"fmt"
+	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -202,3 +204,212 @@ func TestOapiRequestValidator(t *testing.T) {
 		called = false
 	}
 }
+
+func TestOapiRequestValidatorWithOptionsMultiError(t *testing.T) {
+	swagger, err := openapi3.NewLoader().LoadFromData([]byte(testSchema))
+	require.NoError(t, err, "Error initializing swagger")
+
+	g := gin.New()
+
+	// Set up an authenticator to check authenticated function. It will allow
+	// access to "someScope", but disallow others.
+	options := Options{
+		Options: openapi3filter.Options{
+			ExcludeRequestBody:    false,
+			ExcludeResponseBody:   false,
+			IncludeResponseStatus: true,
+			MultiError:            true,
+		},
+	}
+
+	// register middleware
+	g.Use(OapiRequestValidatorWithOptions(swagger, &options))
+
+	called := false
+
+	// Install a request handler for /resource. We want to make sure it doesn't
+	// get called.
+	g.GET("/multiparamresource", func(c *gin.Context) {
+		called = true
+	})
+
+	// Let's send a good request, it should pass
+	{
+		rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=50&id2=50")
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.True(t, called, "Handler should have been called")
+		called = false
+	}
+
+	// Let's send a request with a missing parameter, it should return
+	// a bad status
+	{
+		rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=50")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "multiple errors encountered")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a 2 missing parameters, it should return
+	// a bad status
+	{
+		rec := doGet(t, g, "http://deepmap.ai/multiparamresource")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "multiple errors encountered")
+			assert.Contains(t, string(body), "parameter \\\"id\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a 1 missing parameter, and another outside
+	// or the parameters. It should return a bad status
+	{
+		rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=500")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "multiple errors encountered")
+			assert.Contains(t, string(body), "parameter \\\"id\\\"")
+			assert.Contains(t, string(body), "number must be at most 100")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a parameters that do not meet spec. It should
+	// return a bad status
+	{
+		rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=abc&id2=1")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "multiple errors encountered")
+			assert.Contains(t, string(body), "parameter \\\"id\\\"")
+			assert.Contains(t, string(body), "parsing \\\"abc\\\": invalid syntax")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "number must be at least 10")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+}
+
+func TestOapiRequestValidatorWithOptionsMultiErrorAndCustomHandler(t *testing.T) {
+	swagger, err := openapi3.NewLoader().LoadFromData([]byte(testSchema))
+	require.NoError(t, err, "Error initializing swagger")
+
+	g := gin.New()
+
+	// Set up an authenticator to check authenticated function. It will allow
+	// access to "someScope", but disallow others.
+	options := Options{
+		Options: openapi3filter.Options{
+			ExcludeRequestBody:    false,
+			ExcludeResponseBody:   false,
+			IncludeResponseStatus: true,
+			MultiError:            true,
+		},
+		MultiErrorHandler: func(me openapi3.MultiError) error {
+			return fmt.Errorf("Bad stuff -  %s", me.Error())
+		},
+	}
+
+	// register middleware
+	g.Use(OapiRequestValidatorWithOptions(swagger, &options))
+
+	called := false
+
+	// Install a request handler for /resource. We want to make sure it doesn't
+	// get called.
+	g.GET("/multiparamresource", func(c *gin.Context) {
+		called = true
+	})
+
+	// Let's send a good request, it should pass
+	{
+		rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=50&id2=50")
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.True(t, called, "Handler should have been called")
+		called = false
+	}
+
+	// Let's send a request with a missing parameter, it should return
+	// a bad status
+	{
+		rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=50")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "Bad stuff")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a 2 missing parameters, it should return
+	// a bad status
+	{
+		rec := doGet(t, g, "http://deepmap.ai/multiparamresource")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "Bad stuff")
+			assert.Contains(t, string(body), "parameter \\\"id\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a 1 missing parameter, and another outside
+	// or the parameters. It should return a bad status
+	{
+		rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=500")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "Bad stuff")
+			assert.Contains(t, string(body), "parameter \\\"id\\\"")
+			assert.Contains(t, string(body), "number must be at most 100")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a parameters that do not meet spec. It should
+	// return a bad status
+	{
+		rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=abc&id2=1")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "Bad stuff")
+			assert.Contains(t, string(body), "parameter \\\"id\\\"")
+			assert.Contains(t, string(body), "parsing \\\"abc\\\": invalid syntax")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "number must be at least 10")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+}
diff --git a/pkg/gin-middleware/test_spec.yaml b/pkg/gin-middleware/test_spec.yaml
index f2ce175..3a7b169 100644
--- a/pkg/gin-middleware/test_spec.yaml
+++ b/pkg/gin-middleware/test_spec.yaml
@@ -66,6 +66,35 @@ paths:
       responses:
         '401':
           description: no content
+  /multiparamresource:
+    get:
+      operationId: getResource
+      parameters:
+        - name: id
+          in: query
+          required: true
+          schema:
+            type: integer
+            minimum: 10
+            maximum: 100
+        - name: id2
+          required: true
+          in: query
+          schema:
+            type: integer
+            minimum: 10
+            maximum: 100
+      responses:
+        '200':
+            description: success
+            content:
+              application/json:
+                schema:
+                  properties:
+                    name:
+                      type: string
+                    id:
+                      type: integer
 components:
   securitySchemes:
     BearerAuth:
diff --git a/pkg/middleware/oapi_validate_test.go b/pkg/middleware/oapi_validate_test.go
index 5c3989b..80a221d 100644
--- a/pkg/middleware/oapi_validate_test.go
+++ b/pkg/middleware/oapi_validate_test.go
@@ -18,6 +18,7 @@ import (
 	"context"
 	_ "embed"
 	"errors"
+	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -206,6 +207,215 @@ func TestOapiRequestValidator(t *testing.T) {
 	}
 }
 
+func TestOapiRequestValidatorWithOptionsMultiError(t *testing.T) {
+	swagger, err := openapi3.NewLoader().LoadFromData([]byte(testSchema))
+	require.NoError(t, err, "Error initializing swagger")
+
+	// Create a new echo router
+	e := echo.New()
+
+	// Set up an authenticator to check authenticated function. It will allow
+	// access to "someScope", but disallow others.
+	options := Options{
+		Options: openapi3filter.Options{
+			ExcludeRequestBody:    false,
+			ExcludeResponseBody:   false,
+			IncludeResponseStatus: true,
+			MultiError:            true,
+		},
+	}
+
+	// register middleware
+	e.Use(OapiRequestValidatorWithOptions(swagger, &options))
+
+	called := false
+
+	// Install a request handler for /resource. We want to make sure it doesn't
+	// get called.
+	e.GET("/multiparamresource", func(c echo.Context) error {
+		called = true
+		return nil
+	})
+
+	// Let's send a good request, it should pass
+	{
+		rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=50&id2=50")
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.True(t, called, "Handler should have been called")
+		called = false
+	}
+
+	// Let's send a request with a missing parameter, it should return
+	// a bad status
+	{
+		rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=50")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a 2 missing parameters, it should return
+	// a bad status
+	{
+		rec := doGet(t, e, "http://deepmap.ai/multiparamresource")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \\\"id\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a 1 missing parameter, and another outside
+	// or the parameters. It should return a bad status
+	{
+		rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=500")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \\\"id\\\"")
+			assert.Contains(t, string(body), "number must be at most 100")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a parameters that do not meet spec. It should
+	// return a bad status
+	{
+		rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=abc&id2=1")
+		assert.Equal(t, http.StatusBadRequest, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \\\"id\\\"")
+			assert.Contains(t, string(body), "parsing \\\"abc\\\": invalid syntax")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "number must be at least 10")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+}
+
+func TestOapiRequestValidatorWithOptionsMultiErrorAndCustomHandler(t *testing.T) {
+	swagger, err := openapi3.NewLoader().LoadFromData([]byte(testSchema))
+	require.NoError(t, err, "Error initializing swagger")
+
+	// Create a new echo router
+	e := echo.New()
+
+	// Set up an authenticator to check authenticated function. It will allow
+	// access to "someScope", but disallow others.
+	options := Options{
+		Options: openapi3filter.Options{
+			ExcludeRequestBody:    false,
+			ExcludeResponseBody:   false,
+			IncludeResponseStatus: true,
+			MultiError:            true,
+		},
+		MultiErrorHandler: func(me openapi3.MultiError) *echo.HTTPError {
+			return &echo.HTTPError{
+				Code:     http.StatusTeapot,
+				Message:  me.Error(),
+				Internal: me,
+			}
+		},
+	}
+
+	// register middleware
+	e.Use(OapiRequestValidatorWithOptions(swagger, &options))
+
+	called := false
+
+	// Install a request handler for /resource. We want to make sure it doesn't
+	// get called.
+	e.GET("/multiparamresource", func(c echo.Context) error {
+		called = true
+		return nil
+	})
+
+	// Let's send a good request, it should pass
+	{
+		rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=50&id2=50")
+		assert.Equal(t, http.StatusOK, rec.Code)
+		assert.True(t, called, "Handler should have been called")
+		called = false
+	}
+
+	// Let's send a request with a missing parameter, it should return
+	// a bad status
+	{
+		rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=50")
+		assert.Equal(t, http.StatusTeapot, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a 2 missing parameters, it should return
+	// a bad status
+	{
+		rec := doGet(t, e, "http://deepmap.ai/multiparamresource")
+		assert.Equal(t, http.StatusTeapot, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \\\"id\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a 1 missing parameter, and another outside
+	// or the parameters. It should return a bad status
+	{
+		rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=500")
+		assert.Equal(t, http.StatusTeapot, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \\\"id\\\"")
+			assert.Contains(t, string(body), "number must be at most 100")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "value is required but missing")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+
+	// Let's send a request with a parameters that do not meet spec. It should
+	// return a bad status
+	{
+		rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=abc&id2=1")
+		assert.Equal(t, http.StatusTeapot, rec.Code)
+		body, err := ioutil.ReadAll(rec.Body)
+		if assert.NoError(t, err) {
+			assert.Contains(t, string(body), "parameter \\\"id\\\"")
+			assert.Contains(t, string(body), "parsing \\\"abc\\\": invalid syntax")
+			assert.Contains(t, string(body), "parameter \\\"id2\\\"")
+			assert.Contains(t, string(body), "number must be at least 10")
+		}
+		assert.False(t, called, "Handler should not have been called")
+		called = false
+	}
+}
+
 func TestGetSkipperFromOptions(t *testing.T) {
 
 	options := new(Options)
diff --git a/pkg/middleware/test_spec.yaml b/pkg/middleware/test_spec.yaml
index a279068..7c8ea6a 100644
--- a/pkg/middleware/test_spec.yaml
+++ b/pkg/middleware/test_spec.yaml
@@ -66,6 +66,35 @@ paths:
       responses:
         '401':
           description: no content
+  /multiparamresource:
+    get:
+      operationId: getResource
+      parameters:
+        - name: id
+          in: query
+          required: true
+          schema:
+            type: integer
+            minimum: 10
+            maximum: 100
+        - name: id2
+          required: true
+          in: query
+          schema:
+            type: integer
+            minimum: 10
+            maximum: 100
+      responses:
+        '200':
+            description: success
+            content:
+              application/json:
+                schema:
+                  properties:
+                    name:
+                      type: string
+                    id:
+                      type: integer
 components:
   securitySchemes:
     BearerAuth: