From 0d866a3546b1b780a5d697f276be209af834e205 Mon Sep 17 00:00:00 2001 From: BOTecko <286021104+BOTecko@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:06:23 -0300 Subject: [PATCH] fix: reuse request body string across schema validation errors string([]byte) copies the payload. The fallback that fills ReferenceObject ran that conversion once per flattened violation, so an 8MiB object with hundreds of field errors allocated gigabytes per request. Cache the fallback string once per validation pass in request, response, and schema paths. Also convert renderedSchema once in extractBasicErrors. Validation results are unchanged. --- requests/reference_object_copy_test.go | 73 ++++++++++++++++++++++++++ requests/validate_request.go | 6 ++- responses/validate_response.go | 6 ++- schema_validation/validate_schema.go | 9 +++- 4 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 requests/reference_object_copy_test.go diff --git a/requests/reference_object_copy_test.go b/requests/reference_object_copy_test.go new file mode 100644 index 0000000..c61fd16 --- /dev/null +++ b/requests/reference_object_copy_test.go @@ -0,0 +1,73 @@ +// Copyright 2023-2026 Princess Beef Heavy Industries, LLC / Dave Shanley +// SPDX-License-Identifier: MIT + +package requests + +import ( + "fmt" + "strings" + "testing" + "unsafe" + + liberrors "github.com/pb33f/libopenapi-validator/errors" + "github.com/pb33f/testify/assert" + "github.com/pb33f/testify/require" +) + +func TestReferenceObjectCopiesRequestBodyPerViolation(t *testing.T) { + const ( + violations = 80 + paddingLen = 256 * 1024 + ) + + body := largeObjectWithTypedFields(paddingLen, violations) + schema := parseSchemaFromSpec(t, objectSchemaRequiringStrings(violations), 3.1) + + valid, errs := ValidateRequestSchema(&ValidateRequestSchemaInput{ + Request: postRequestWithBody(body), + Schema: schema, + Version: 3.1, + }) + require.False(t, valid) + require.Len(t, errs, 1) + require.GreaterOrEqual(t, len(errs[0].SchemaValidationErrors), violations) + + ptrs := uniqueReferenceObjectBackings(errs[0].SchemaValidationErrors) + t.Logf("body_bytes=%d schema_violations=%d unique_ReferenceObject_backings=%d", + len(body), len(errs[0].SchemaValidationErrors), len(ptrs)) + + assert.Equal(t, 1, len(ptrs)) +} + +func objectSchemaRequiringStrings(fields int) string { + var b strings.Builder + b.WriteString("type: object\nproperties:\n pad:\n type: string\n") + for i := 0; i < fields; i++ { + fmt.Fprintf(&b, " f%d:\n type: string\n", i) + } + return b.String() +} + +func largeObjectWithTypedFields(paddingLen, fields int) string { + var b strings.Builder + b.Grow(paddingLen + fields*16 + 32) + b.WriteString(`{"pad":"`) + b.WriteString(strings.Repeat("x", paddingLen)) + b.WriteString(`"`) + for i := 0; i < fields; i++ { + fmt.Fprintf(&b, `,"f%d":1`, i) + } + b.WriteString(`}`) + return b.String() +} + +func uniqueReferenceObjectBackings(fails []*liberrors.SchemaValidationFailure) map[uintptr]struct{} { + seen := make(map[uintptr]struct{}, len(fails)) + for _, fail := range fails { + if fail == nil || fail.ReferenceObject == "" { + continue + } + seen[uintptr(unsafe.Pointer(unsafe.StringData(fail.ReferenceObject)))] = struct{}{} + } + return seen +} diff --git a/requests/validate_request.go b/requests/validate_request.go index e760f85..9f74197 100644 --- a/requests/validate_request.go +++ b/requests/validate_request.go @@ -206,6 +206,7 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*liberror schFlatErrs := helpers.FlattenSchemaOutputErrors(jk.DetailedOutput()) renderedNode, resourceNodes := schema_validation.DiagnosticLocationNodes(renderedSchema, cachedNode, resourceNodes) + var fallbackReferenceObject string for q := range schFlatErrs { er := schFlatErrs[q] @@ -240,7 +241,10 @@ func ValidateRequestSchema(input *ValidateRequestSchemaInput) (bool, []*liberror } } if referenceObject == "" { - referenceObject = string(requestBody) + if fallbackReferenceObject == "" { + fallbackReferenceObject = string(requestBody) + } + referenceObject = fallbackReferenceObject } errMsg := er.Error.Kind.LocalizedString(message.NewPrinter(language.Tag{})) diff --git a/responses/validate_response.go b/responses/validate_response.go index e307b86..6032ffb 100644 --- a/responses/validate_response.go +++ b/responses/validate_response.go @@ -244,6 +244,7 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*liberr _ = yaml.Unmarshal(renderedSchema, renderedNode) } + var fallbackReferenceObject string for q := range schFlatErrs { er := schFlatErrs[q] @@ -276,7 +277,10 @@ func ValidateResponseSchema(input *ValidateResponseSchemaInput) (bool, []*liberr } } if referenceObject == "" { - referenceObject = string(responseBody) + if fallbackReferenceObject == "" { + fallbackReferenceObject = string(responseBody) + } + referenceObject = fallbackReferenceObject } violation := &liberrors.SchemaValidationFailure{ diff --git a/schema_validation/validate_schema.go b/schema_validation/validate_schema.go index 425eb59..8e988b7 100644 --- a/schema_validation/validate_schema.go +++ b/schema_validation/validate_schema.go @@ -258,6 +258,8 @@ func extractBasicErrors(schFlatErrs []jsonschema.OutputUnit, propertyInfo := extractPropertyNameFromError(jk) rootNode, resourceNodes := DiagnosticLocationNodes(renderedSchema, renderedNode, resourceNodes) + var fallbackReferenceObject string + referenceSchema := string(renderedSchema) for q := range schFlatErrs { er := schFlatErrs[q] @@ -292,7 +294,10 @@ func extractBasicErrors(schFlatErrs []jsonschema.OutputUnit, } } if referenceObject == "" { - referenceObject = string(payload) + if fallbackReferenceObject == "" { + fallbackReferenceObject = string(payload) + } + referenceObject = fallbackReferenceObject } violation := &liberrors.SchemaValidationFailure{ @@ -301,7 +306,7 @@ func extractBasicErrors(schFlatErrs []jsonschema.OutputUnit, FieldPath: helpers.ExtractJSONPathFromStringLocation(er.InstanceLocation), InstancePath: helpers.ConvertStringLocationToPathSegments(er.InstanceLocation), KeywordLocation: er.KeywordLocation, - ReferenceSchema: string(renderedSchema), + ReferenceSchema: referenceSchema, ReferenceObject: referenceObject, OriginalJsonSchemaError: jk, }