Skip to content

Commit 2071a52

Browse files
authored
handle oapi3.MultiError messages (#572)
* 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. * Convert echo middleware to default MultiErrorHandler * Convert gin middleware to default MultiErrorHandler * Convert chi middleware to default MultiErrorHandler
1 parent a0bc170 commit 2071a52

3 files changed

Lines changed: 280 additions & 5 deletions

File tree

oapi_validate.go

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package middleware
1616

1717
import (
1818
"context"
19+
"errors"
1920
"fmt"
2021
"io/ioutil"
2122
"net/http"
@@ -61,14 +62,18 @@ func OapiRequestValidator(swagger *openapi3.T) echo.MiddlewareFunc {
6162
// ErrorHandler is called when there is an error in validation
6263
type ErrorHandler func(c echo.Context, err *echo.HTTPError) error
6364

65+
// MultiErrorHandler is called when oapi returns a MultiError type
66+
type MultiErrorHandler func(openapi3.MultiError) *echo.HTTPError
67+
6468
// Options to customize request validation. These are passed through to
6569
// openapi3filter.
6670
type Options struct {
67-
ErrorHandler ErrorHandler
68-
Options openapi3filter.Options
69-
ParamDecoder openapi3filter.ContentParameterDecoder
70-
UserData interface{}
71-
Skipper echomiddleware.Skipper
71+
ErrorHandler ErrorHandler
72+
Options openapi3filter.Options
73+
ParamDecoder openapi3filter.ContentParameterDecoder
74+
UserData interface{}
75+
Skipper echomiddleware.Skipper
76+
MultiErrorHandler MultiErrorHandler
7277
}
7378

7479
// OapiRequestValidatorWithOptions creates a validator from a swagger object, with validation options
@@ -136,6 +141,12 @@ func ValidateRequestFromContext(ctx echo.Context, router routers.Router, options
136141

137142
err = openapi3filter.ValidateRequest(requestContext, validationInput)
138143
if err != nil {
144+
me := openapi3.MultiError{}
145+
if errors.As(err, &me) {
146+
errFunc := getMultiErrorHandlerFromOptions(options)
147+
return errFunc(me)
148+
}
149+
139150
switch e := err.(type) {
140151
case *openapi3filter.RequestError:
141152
// We've got a bad request
@@ -202,3 +213,28 @@ func getSkipperFromOptions(options *Options) echomiddleware.Skipper {
202213

203214
return options.Skipper
204215
}
216+
217+
// attempt to get the MultiErrorHandler from the options. If it is not set,
218+
// return a default handler
219+
func getMultiErrorHandlerFromOptions(options *Options) MultiErrorHandler {
220+
if options == nil {
221+
return defaultMultiErrorHandler
222+
}
223+
224+
if options.MultiErrorHandler == nil {
225+
return defaultMultiErrorHandler
226+
}
227+
228+
return options.MultiErrorHandler
229+
}
230+
231+
// defaultMultiErrorHandler returns a StatusBadRequest (400) and a list
232+
// of all of the errors. This method is called if there are no other
233+
// methods defined on the options.
234+
func defaultMultiErrorHandler(me openapi3.MultiError) *echo.HTTPError {
235+
return &echo.HTTPError{
236+
Code: http.StatusBadRequest,
237+
Message: me.Error(),
238+
Internal: me,
239+
}
240+
}

oapi_validate_test.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
_ "embed"
2020
"errors"
21+
"io/ioutil"
2122
"net/http"
2223
"net/http/httptest"
2324
"net/url"
@@ -206,6 +207,215 @@ func TestOapiRequestValidator(t *testing.T) {
206207
}
207208
}
208209

210+
func TestOapiRequestValidatorWithOptionsMultiError(t *testing.T) {
211+
swagger, err := openapi3.NewLoader().LoadFromData([]byte(testSchema))
212+
require.NoError(t, err, "Error initializing swagger")
213+
214+
// Create a new echo router
215+
e := echo.New()
216+
217+
// Set up an authenticator to check authenticated function. It will allow
218+
// access to "someScope", but disallow others.
219+
options := Options{
220+
Options: openapi3filter.Options{
221+
ExcludeRequestBody: false,
222+
ExcludeResponseBody: false,
223+
IncludeResponseStatus: true,
224+
MultiError: true,
225+
},
226+
}
227+
228+
// register middleware
229+
e.Use(OapiRequestValidatorWithOptions(swagger, &options))
230+
231+
called := false
232+
233+
// Install a request handler for /resource. We want to make sure it doesn't
234+
// get called.
235+
e.GET("/multiparamresource", func(c echo.Context) error {
236+
called = true
237+
return nil
238+
})
239+
240+
// Let's send a good request, it should pass
241+
{
242+
rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=50&id2=50")
243+
assert.Equal(t, http.StatusOK, rec.Code)
244+
assert.True(t, called, "Handler should have been called")
245+
called = false
246+
}
247+
248+
// Let's send a request with a missing parameter, it should return
249+
// a bad status
250+
{
251+
rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=50")
252+
assert.Equal(t, http.StatusBadRequest, rec.Code)
253+
body, err := ioutil.ReadAll(rec.Body)
254+
if assert.NoError(t, err) {
255+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
256+
assert.Contains(t, string(body), "value is required but missing")
257+
}
258+
assert.False(t, called, "Handler should not have been called")
259+
called = false
260+
}
261+
262+
// Let's send a request with a 2 missing parameters, it should return
263+
// a bad status
264+
{
265+
rec := doGet(t, e, "http://deepmap.ai/multiparamresource")
266+
assert.Equal(t, http.StatusBadRequest, rec.Code)
267+
body, err := ioutil.ReadAll(rec.Body)
268+
if assert.NoError(t, err) {
269+
assert.Contains(t, string(body), "parameter \\\"id\\\"")
270+
assert.Contains(t, string(body), "value is required but missing")
271+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
272+
assert.Contains(t, string(body), "value is required but missing")
273+
}
274+
assert.False(t, called, "Handler should not have been called")
275+
called = false
276+
}
277+
278+
// Let's send a request with a 1 missing parameter, and another outside
279+
// or the parameters. It should return a bad status
280+
{
281+
rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=500")
282+
assert.Equal(t, http.StatusBadRequest, rec.Code)
283+
body, err := ioutil.ReadAll(rec.Body)
284+
if assert.NoError(t, err) {
285+
assert.Contains(t, string(body), "parameter \\\"id\\\"")
286+
assert.Contains(t, string(body), "number must be at most 100")
287+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
288+
assert.Contains(t, string(body), "value is required but missing")
289+
}
290+
assert.False(t, called, "Handler should not have been called")
291+
called = false
292+
}
293+
294+
// Let's send a request with a parameters that do not meet spec. It should
295+
// return a bad status
296+
{
297+
rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=abc&id2=1")
298+
assert.Equal(t, http.StatusBadRequest, rec.Code)
299+
body, err := ioutil.ReadAll(rec.Body)
300+
if assert.NoError(t, err) {
301+
assert.Contains(t, string(body), "parameter \\\"id\\\"")
302+
assert.Contains(t, string(body), "parsing \\\"abc\\\": invalid syntax")
303+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
304+
assert.Contains(t, string(body), "number must be at least 10")
305+
}
306+
assert.False(t, called, "Handler should not have been called")
307+
called = false
308+
}
309+
}
310+
311+
func TestOapiRequestValidatorWithOptionsMultiErrorAndCustomHandler(t *testing.T) {
312+
swagger, err := openapi3.NewLoader().LoadFromData([]byte(testSchema))
313+
require.NoError(t, err, "Error initializing swagger")
314+
315+
// Create a new echo router
316+
e := echo.New()
317+
318+
// Set up an authenticator to check authenticated function. It will allow
319+
// access to "someScope", but disallow others.
320+
options := Options{
321+
Options: openapi3filter.Options{
322+
ExcludeRequestBody: false,
323+
ExcludeResponseBody: false,
324+
IncludeResponseStatus: true,
325+
MultiError: true,
326+
},
327+
MultiErrorHandler: func(me openapi3.MultiError) *echo.HTTPError {
328+
return &echo.HTTPError{
329+
Code: http.StatusTeapot,
330+
Message: me.Error(),
331+
Internal: me,
332+
}
333+
},
334+
}
335+
336+
// register middleware
337+
e.Use(OapiRequestValidatorWithOptions(swagger, &options))
338+
339+
called := false
340+
341+
// Install a request handler for /resource. We want to make sure it doesn't
342+
// get called.
343+
e.GET("/multiparamresource", func(c echo.Context) error {
344+
called = true
345+
return nil
346+
})
347+
348+
// Let's send a good request, it should pass
349+
{
350+
rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=50&id2=50")
351+
assert.Equal(t, http.StatusOK, rec.Code)
352+
assert.True(t, called, "Handler should have been called")
353+
called = false
354+
}
355+
356+
// Let's send a request with a missing parameter, it should return
357+
// a bad status
358+
{
359+
rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=50")
360+
assert.Equal(t, http.StatusTeapot, rec.Code)
361+
body, err := ioutil.ReadAll(rec.Body)
362+
if assert.NoError(t, err) {
363+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
364+
assert.Contains(t, string(body), "value is required but missing")
365+
}
366+
assert.False(t, called, "Handler should not have been called")
367+
called = false
368+
}
369+
370+
// Let's send a request with a 2 missing parameters, it should return
371+
// a bad status
372+
{
373+
rec := doGet(t, e, "http://deepmap.ai/multiparamresource")
374+
assert.Equal(t, http.StatusTeapot, rec.Code)
375+
body, err := ioutil.ReadAll(rec.Body)
376+
if assert.NoError(t, err) {
377+
assert.Contains(t, string(body), "parameter \\\"id\\\"")
378+
assert.Contains(t, string(body), "value is required but missing")
379+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
380+
assert.Contains(t, string(body), "value is required but missing")
381+
}
382+
assert.False(t, called, "Handler should not have been called")
383+
called = false
384+
}
385+
386+
// Let's send a request with a 1 missing parameter, and another outside
387+
// or the parameters. It should return a bad status
388+
{
389+
rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=500")
390+
assert.Equal(t, http.StatusTeapot, rec.Code)
391+
body, err := ioutil.ReadAll(rec.Body)
392+
if assert.NoError(t, err) {
393+
assert.Contains(t, string(body), "parameter \\\"id\\\"")
394+
assert.Contains(t, string(body), "number must be at most 100")
395+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
396+
assert.Contains(t, string(body), "value is required but missing")
397+
}
398+
assert.False(t, called, "Handler should not have been called")
399+
called = false
400+
}
401+
402+
// Let's send a request with a parameters that do not meet spec. It should
403+
// return a bad status
404+
{
405+
rec := doGet(t, e, "http://deepmap.ai/multiparamresource?id=abc&id2=1")
406+
assert.Equal(t, http.StatusTeapot, rec.Code)
407+
body, err := ioutil.ReadAll(rec.Body)
408+
if assert.NoError(t, err) {
409+
assert.Contains(t, string(body), "parameter \\\"id\\\"")
410+
assert.Contains(t, string(body), "parsing \\\"abc\\\": invalid syntax")
411+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
412+
assert.Contains(t, string(body), "number must be at least 10")
413+
}
414+
assert.False(t, called, "Handler should not have been called")
415+
called = false
416+
}
417+
}
418+
209419
func TestGetSkipperFromOptions(t *testing.T) {
210420

211421
options := new(Options)

test_spec.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,35 @@ paths:
6666
responses:
6767
'401':
6868
description: no content
69+
/multiparamresource:
70+
get:
71+
operationId: getResource
72+
parameters:
73+
- name: id
74+
in: query
75+
required: true
76+
schema:
77+
type: integer
78+
minimum: 10
79+
maximum: 100
80+
- name: id2
81+
required: true
82+
in: query
83+
schema:
84+
type: integer
85+
minimum: 10
86+
maximum: 100
87+
responses:
88+
'200':
89+
description: success
90+
content:
91+
application/json:
92+
schema:
93+
properties:
94+
name:
95+
type: string
96+
id:
97+
type: integer
6998
components:
7099
securitySchemes:
71100
BearerAuth:

0 commit comments

Comments
 (0)