Skip to content

Commit 163eee8

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 67e3b70 commit 163eee8

File tree

3 files changed

+275
-4
lines changed

3 files changed

+275
-4
lines changed

oapi_validate.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,17 @@ func OapiRequestValidator(swagger *openapi3.T) gin.HandlerFunc {
5959
// ErrorHandler is called when there is an error in validation
6060
type ErrorHandler func(c *gin.Context, message string, statusCode int)
6161

62+
// MultiErrorHandler is called when oapi returns a MultiError type
63+
type MultiErrorHandler func(openapi3.MultiError) error
64+
6265
// Options to customize request validation. These are passed through to
6366
// openapi3filter.
6467
type Options struct {
65-
ErrorHandler ErrorHandler
66-
Options openapi3filter.Options
67-
ParamDecoder openapi3filter.ContentParameterDecoder
68-
UserData interface{}
68+
ErrorHandler ErrorHandler
69+
Options openapi3filter.Options
70+
ParamDecoder openapi3filter.ContentParameterDecoder
71+
UserData interface{}
72+
MultiErrorHandler MultiErrorHandler
6973
}
7074

7175
// Create a validator from a swagger object, with validation options
@@ -128,6 +132,12 @@ func ValidateRequestFromContext(c *gin.Context, router routers.Router, options *
128132

129133
err = openapi3filter.ValidateRequest(requestContext, validationInput)
130134
if err != nil {
135+
me := openapi3.MultiError{}
136+
if errors.As(err, &me) {
137+
errFunc := getMultiErrorHandlerFromOptions(options)
138+
return errFunc(me)
139+
}
140+
131141
switch e := err.(type) {
132142
case *openapi3filter.RequestError:
133143
// We've got a bad request
@@ -163,3 +173,24 @@ func GetGinContext(c context.Context) *gin.Context {
163173
func GetUserData(c context.Context) interface{} {
164174
return c.Value(UserDataKey)
165175
}
176+
177+
// attempt to get the MultiErrorHandler from the options. If it is not set,
178+
// return a default handler
179+
func getMultiErrorHandlerFromOptions(options *Options) MultiErrorHandler {
180+
if options == nil {
181+
return defaultMultiErrorHandler
182+
}
183+
184+
if options.MultiErrorHandler == nil {
185+
return defaultMultiErrorHandler
186+
}
187+
188+
return options.MultiErrorHandler
189+
}
190+
191+
// defaultMultiErrorHandler returns a StatusBadRequest (400) and a list
192+
// of all of the errors. This method is called if there are no other
193+
// methods defined on the options.
194+
func defaultMultiErrorHandler(me openapi3.MultiError) error {
195+
return fmt.Errorf("multiple errors encountered: %s", me)
196+
}

oapi_validate_test.go

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"context"
1919
_ "embed"
2020
"errors"
21+
"fmt"
22+
"io/ioutil"
2123
"net/http"
2224
"net/http/httptest"
2325
"net/url"
@@ -202,3 +204,212 @@ func TestOapiRequestValidator(t *testing.T) {
202204
called = false
203205
}
204206
}
207+
208+
func TestOapiRequestValidatorWithOptionsMultiError(t *testing.T) {
209+
swagger, err := openapi3.NewLoader().LoadFromData([]byte(testSchema))
210+
require.NoError(t, err, "Error initializing swagger")
211+
212+
g := gin.New()
213+
214+
// Set up an authenticator to check authenticated function. It will allow
215+
// access to "someScope", but disallow others.
216+
options := Options{
217+
Options: openapi3filter.Options{
218+
ExcludeRequestBody: false,
219+
ExcludeResponseBody: false,
220+
IncludeResponseStatus: true,
221+
MultiError: true,
222+
},
223+
}
224+
225+
// register middleware
226+
g.Use(OapiRequestValidatorWithOptions(swagger, &options))
227+
228+
called := false
229+
230+
// Install a request handler for /resource. We want to make sure it doesn't
231+
// get called.
232+
g.GET("/multiparamresource", func(c *gin.Context) {
233+
called = true
234+
})
235+
236+
// Let's send a good request, it should pass
237+
{
238+
rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=50&id2=50")
239+
assert.Equal(t, http.StatusOK, rec.Code)
240+
assert.True(t, called, "Handler should have been called")
241+
called = false
242+
}
243+
244+
// Let's send a request with a missing parameter, it should return
245+
// a bad status
246+
{
247+
rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=50")
248+
assert.Equal(t, http.StatusBadRequest, rec.Code)
249+
body, err := ioutil.ReadAll(rec.Body)
250+
if assert.NoError(t, err) {
251+
assert.Contains(t, string(body), "multiple errors encountered")
252+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
253+
assert.Contains(t, string(body), "value is required but missing")
254+
}
255+
assert.False(t, called, "Handler should not have been called")
256+
called = false
257+
}
258+
259+
// Let's send a request with a 2 missing parameters, it should return
260+
// a bad status
261+
{
262+
rec := doGet(t, g, "http://deepmap.ai/multiparamresource")
263+
assert.Equal(t, http.StatusBadRequest, rec.Code)
264+
body, err := ioutil.ReadAll(rec.Body)
265+
if assert.NoError(t, err) {
266+
assert.Contains(t, string(body), "multiple errors encountered")
267+
assert.Contains(t, string(body), "parameter \\\"id\\\"")
268+
assert.Contains(t, string(body), "value is required but missing")
269+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
270+
assert.Contains(t, string(body), "value is required but missing")
271+
}
272+
assert.False(t, called, "Handler should not have been called")
273+
called = false
274+
}
275+
276+
// Let's send a request with a 1 missing parameter, and another outside
277+
// or the parameters. It should return a bad status
278+
{
279+
rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=500")
280+
assert.Equal(t, http.StatusBadRequest, rec.Code)
281+
body, err := ioutil.ReadAll(rec.Body)
282+
if assert.NoError(t, err) {
283+
assert.Contains(t, string(body), "multiple errors encountered")
284+
assert.Contains(t, string(body), "parameter \\\"id\\\"")
285+
assert.Contains(t, string(body), "number must be at most 100")
286+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
287+
assert.Contains(t, string(body), "value is required but missing")
288+
}
289+
assert.False(t, called, "Handler should not have been called")
290+
called = false
291+
}
292+
293+
// Let's send a request with a parameters that do not meet spec. It should
294+
// return a bad status
295+
{
296+
rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=abc&id2=1")
297+
assert.Equal(t, http.StatusBadRequest, rec.Code)
298+
body, err := ioutil.ReadAll(rec.Body)
299+
if assert.NoError(t, err) {
300+
assert.Contains(t, string(body), "multiple errors encountered")
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+
g := gin.New()
316+
317+
// Set up an authenticator to check authenticated function. It will allow
318+
// access to "someScope", but disallow others.
319+
options := Options{
320+
Options: openapi3filter.Options{
321+
ExcludeRequestBody: false,
322+
ExcludeResponseBody: false,
323+
IncludeResponseStatus: true,
324+
MultiError: true,
325+
},
326+
MultiErrorHandler: func(me openapi3.MultiError) error {
327+
return fmt.Errorf("Bad stuff - %s", me.Error())
328+
},
329+
}
330+
331+
// register middleware
332+
g.Use(OapiRequestValidatorWithOptions(swagger, &options))
333+
334+
called := false
335+
336+
// Install a request handler for /resource. We want to make sure it doesn't
337+
// get called.
338+
g.GET("/multiparamresource", func(c *gin.Context) {
339+
called = true
340+
})
341+
342+
// Let's send a good request, it should pass
343+
{
344+
rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=50&id2=50")
345+
assert.Equal(t, http.StatusOK, rec.Code)
346+
assert.True(t, called, "Handler should have been called")
347+
called = false
348+
}
349+
350+
// Let's send a request with a missing parameter, it should return
351+
// a bad status
352+
{
353+
rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=50")
354+
assert.Equal(t, http.StatusBadRequest, rec.Code)
355+
body, err := ioutil.ReadAll(rec.Body)
356+
if assert.NoError(t, err) {
357+
assert.Contains(t, string(body), "Bad stuff")
358+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
359+
assert.Contains(t, string(body), "value is required but missing")
360+
}
361+
assert.False(t, called, "Handler should not have been called")
362+
called = false
363+
}
364+
365+
// Let's send a request with a 2 missing parameters, it should return
366+
// a bad status
367+
{
368+
rec := doGet(t, g, "http://deepmap.ai/multiparamresource")
369+
assert.Equal(t, http.StatusBadRequest, rec.Code)
370+
body, err := ioutil.ReadAll(rec.Body)
371+
if assert.NoError(t, err) {
372+
assert.Contains(t, string(body), "Bad stuff")
373+
assert.Contains(t, string(body), "parameter \\\"id\\\"")
374+
assert.Contains(t, string(body), "value is required but missing")
375+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
376+
assert.Contains(t, string(body), "value is required but missing")
377+
}
378+
assert.False(t, called, "Handler should not have been called")
379+
called = false
380+
}
381+
382+
// Let's send a request with a 1 missing parameter, and another outside
383+
// or the parameters. It should return a bad status
384+
{
385+
rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=500")
386+
assert.Equal(t, http.StatusBadRequest, rec.Code)
387+
body, err := ioutil.ReadAll(rec.Body)
388+
if assert.NoError(t, err) {
389+
assert.Contains(t, string(body), "Bad stuff")
390+
assert.Contains(t, string(body), "parameter \\\"id\\\"")
391+
assert.Contains(t, string(body), "number must be at most 100")
392+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
393+
assert.Contains(t, string(body), "value is required but missing")
394+
}
395+
assert.False(t, called, "Handler should not have been called")
396+
called = false
397+
}
398+
399+
// Let's send a request with a parameters that do not meet spec. It should
400+
// return a bad status
401+
{
402+
rec := doGet(t, g, "http://deepmap.ai/multiparamresource?id=abc&id2=1")
403+
assert.Equal(t, http.StatusBadRequest, rec.Code)
404+
body, err := ioutil.ReadAll(rec.Body)
405+
if assert.NoError(t, err) {
406+
assert.Contains(t, string(body), "Bad stuff")
407+
assert.Contains(t, string(body), "parameter \\\"id\\\"")
408+
assert.Contains(t, string(body), "parsing \\\"abc\\\": invalid syntax")
409+
assert.Contains(t, string(body), "parameter \\\"id2\\\"")
410+
assert.Contains(t, string(body), "number must be at least 10")
411+
}
412+
assert.False(t, called, "Handler should not have been called")
413+
called = false
414+
}
415+
}

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)