Skip to content

Commit 35bf259

Browse files
committed
Fix #192
non deterministic error handling was the issue.
1 parent 72d4b2d commit 35bf259

File tree

3 files changed

+61
-3
lines changed

3 files changed

+61
-3
lines changed

validator.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
// Copyright 2023 Princess B33f Heavy Industries / Dave Shanley
1+
// Copyright 2023-2025 Princess Beef Heavy Industries, LLC / Dave Shanley
22
// SPDX-License-Identifier: MIT
33

44
package validator
55

66
import (
77
"fmt"
88
"net/http"
9+
"sort"
910
"sync"
1011

1112
"github.com/pb33f/libopenapi"
@@ -292,6 +293,10 @@ func (v *validator) ValidateHttpRequestWithPathItem(request *http.Request, pathI
292293

293294
// wait for all the validations to complete
294295
<-doneChan
296+
297+
// sort errors for deterministic ordering (async validation can return errors in any order)
298+
sortValidationErrors(validationErrors)
299+
295300
return len(validationErrors) == 0, validationErrors
296301
}
297302

@@ -372,6 +377,17 @@ type (
372377
validationFunctionAsync func(control chan struct{}, errorChan chan []*errors.ValidationError)
373378
)
374379

380+
// sortValidationErrors sorts validation errors for deterministic ordering.
381+
// Errors are sorted by validation type first, then by message.
382+
func sortValidationErrors(errs []*errors.ValidationError) {
383+
sort.Slice(errs, func(i, j int) bool {
384+
if errs[i].ValidationType != errs[j].ValidationType {
385+
return errs[i].ValidationType < errs[j].ValidationType
386+
}
387+
return errs[i].Message < errs[j].Message
388+
})
389+
}
390+
375391
// warmSchemaCaches pre-compiles all schemas in the OpenAPI document and stores them in the validator caches.
376392
// This frontloads the compilation cost so that runtime validation doesn't need to compile schemas.
377393
func warmSchemaCaches(

validator_examples_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,7 @@ func ExampleNewValidator_validateHttpRequestSync() {
121121
fmt.Printf("Type: %s, Failure: %s\n", e.ValidationType, e.Message)
122122
}
123123
}
124-
// Type: parameter, Failure: Path parameter 'petId' is not a valid integer
125-
// Output: Type: security, Failure: API Key api_key not found in header
124+
// Output: Type: parameter, Failure: Path parameter 'petId' is not a valid integer
126125
}
127126

128127
func ExampleNewValidator_validateHttpRequestResponse() {

validator_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2395,3 +2395,46 @@ paths:
23952395
})
23962396
assert.Greater(t, count, 0, "Schema cache should have entries from path-level parameters")
23972397
}
2398+
2399+
// TestSortValidationErrors tests that validation errors are sorted deterministically
2400+
func TestSortValidationErrors(t *testing.T) {
2401+
// Create errors in random order
2402+
errs := []*errors.ValidationError{
2403+
{ValidationType: "security", Message: "API Key missing"},
2404+
{ValidationType: "parameter", Message: "Path param invalid"},
2405+
{ValidationType: "request", Message: "Body invalid"},
2406+
{ValidationType: "parameter", Message: "Header missing"},
2407+
{ValidationType: "security", Message: "Auth header missing"},
2408+
}
2409+
2410+
sortValidationErrors(errs)
2411+
2412+
// Verify sorted by validation type first, then by message
2413+
assert.Equal(t, "parameter", errs[0].ValidationType)
2414+
assert.Equal(t, "Header missing", errs[0].Message)
2415+
assert.Equal(t, "parameter", errs[1].ValidationType)
2416+
assert.Equal(t, "Path param invalid", errs[1].Message)
2417+
assert.Equal(t, "request", errs[2].ValidationType)
2418+
assert.Equal(t, "Body invalid", errs[2].Message)
2419+
assert.Equal(t, "security", errs[3].ValidationType)
2420+
assert.Equal(t, "API Key missing", errs[3].Message)
2421+
assert.Equal(t, "security", errs[4].ValidationType)
2422+
assert.Equal(t, "Auth header missing", errs[4].Message)
2423+
}
2424+
2425+
// TestSortValidationErrors_Empty tests sorting empty slice
2426+
func TestSortValidationErrors_Empty(t *testing.T) {
2427+
errs := []*errors.ValidationError{}
2428+
sortValidationErrors(errs)
2429+
assert.Empty(t, errs)
2430+
}
2431+
2432+
// TestSortValidationErrors_SingleElement tests sorting single element slice
2433+
func TestSortValidationErrors_SingleElement(t *testing.T) {
2434+
errs := []*errors.ValidationError{
2435+
{ValidationType: "parameter", Message: "Invalid value"},
2436+
}
2437+
sortValidationErrors(errs)
2438+
assert.Len(t, errs, 1)
2439+
assert.Equal(t, "parameter", errs[0].ValidationType)
2440+
}

0 commit comments

Comments
 (0)