Skip to content

Commit 7a9aee7

Browse files
fix: prevent panic on invalid paths entries during index build (#197)
1 parent f5ae225 commit 7a9aee7

2 files changed

Lines changed: 71 additions & 6 deletions

File tree

openapi/index_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,6 +1220,52 @@ paths:
12201220
assert.Len(t, idx.InlinePathItems, 2, "should have 2 inline path items")
12211221
}
12221222

1223+
func TestBuildIndex_InvalidPathsEntries_NoPanicAndValidationErrors_Success(t *testing.T) {
1224+
t.Parallel()
1225+
ctx := t.Context()
1226+
1227+
yaml := `
1228+
openapi: "3.1.0"
1229+
info:
1230+
title: Test API
1231+
version: 1.0.0
1232+
paths:
1233+
/api/v1/foo: []
1234+
foo: bar
1235+
`
1236+
1237+
doc, validationErrs, err := openapi.Unmarshal(ctx, strings.NewReader(yaml))
1238+
require.NoError(t, err, "unmarshal should succeed")
1239+
require.NotNil(t, doc, "document should still be created")
1240+
require.NotEmpty(t, validationErrs, "invalid paths entries should produce validation errors")
1241+
1242+
var validationErrMessages []string
1243+
for _, validationErr := range validationErrs {
1244+
validationErrMessages = append(validationErrMessages, validationErr.Error())
1245+
}
1246+
1247+
require.Len(t, validationErrMessages, 2, "should report both invalid path entries")
1248+
assert.Contains(t, validationErrMessages[0], "validation-type-mismatch", "should classify invalid path entries as type mismatch")
1249+
assert.Contains(t, validationErrMessages[0], "paths./api/v1/foo", "should point to the invalid path key")
1250+
assert.Contains(t, validationErrMessages[0], "expected `object`, got sequence", "should explain that array path item values are invalid")
1251+
assert.Contains(t, validationErrMessages[1], "validation-type-mismatch", "should classify invalid path entries as type mismatch")
1252+
assert.Contains(t, validationErrMessages[1], "paths.foo", "should point to the invalid non-path key")
1253+
assert.Contains(t, validationErrMessages[1], "expected `object`, got scalar", "should explain that scalar path item values are invalid")
1254+
1255+
var idx *openapi.Index
1256+
assert.NotPanics(t, func() {
1257+
idx = openapi.BuildIndex(ctx, doc, references.ResolveOptions{
1258+
RootDocument: doc,
1259+
TargetDocument: doc,
1260+
TargetLocation: "test.yaml",
1261+
})
1262+
}, "indexing invalid paths entries should not panic")
1263+
1264+
require.NotNil(t, idx, "index should not be nil")
1265+
// Type mismatch validation errors for invalid path entries are surfaced by Unmarshal;
1266+
// BuildIndex should remain panic-free and produce a usable index for callers.
1267+
}
1268+
12231269
func TestBuildIndex_Parameters_Success(t *testing.T) {
12241270
t.Parallel()
12251271
ctx := t.Context()

openapi/sanitize.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"io"
77
"os"
88
"path/filepath"
9+
"reflect"
910

1011
"github.com/speakeasy-api/openapi/extensions"
1112
"github.com/speakeasy-api/openapi/jsonschema/oas3"
@@ -599,8 +600,26 @@ func cleanUnknownPropertiesFromModel(model any) error {
599600
return nil
600601
}
601602

603+
func isNilAny(v any) bool {
604+
if v == nil {
605+
return true
606+
}
607+
608+
rv := reflect.ValueOf(v)
609+
switch rv.Kind() {
610+
case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.Slice:
611+
return rv.IsNil()
612+
default:
613+
return false
614+
}
615+
}
616+
602617
// getCoreModelFromAny attempts to extract a core model from various wrapper types
603618
func getCoreModelFromAny(model any) any {
619+
if isNilAny(model) {
620+
return nil
621+
}
622+
604623
// Try direct core getter
605624
type coreGetter interface {
606625
GetCoreAny() any
@@ -609,7 +628,7 @@ func getCoreModelFromAny(model any) any {
609628
var directCore any
610629
if coreModel, ok := model.(coreGetter); ok {
611630
directCore = coreModel.GetCoreAny()
612-
if directCore != nil {
631+
if !isNilAny(directCore) {
613632
if coreModeler, ok := directCore.(marshaller.CoreModeler); ok {
614633
if len(coreModeler.GetUnknownProperties()) > 0 {
615634
return directCore
@@ -627,9 +646,9 @@ func getCoreModelFromAny(model any) any {
627646

628647
if navigable, ok := model.(navigableNoder); ok {
629648
inner, err := navigable.GetNavigableNode()
630-
if err == nil && inner != nil {
649+
if err == nil && !isNilAny(inner) {
631650
// Recursively try to get core from the inner value
632-
if innerCore := getCoreModelFromAny(inner); innerCore != nil {
651+
if innerCore := getCoreModelFromAny(inner); !isNilAny(innerCore) {
633652
return innerCore
634653
}
635654
}
@@ -641,7 +660,7 @@ func getCoreModelFromAny(model any) any {
641660
// getRootNodeFromAny attempts to extract the root yaml.Node from various OpenAPI types.
642661
// This is used for node-to-operation mapping during indexing.
643662
func getRootNodeFromAny(model any) *yaml.Node {
644-
if model == nil {
663+
if isNilAny(model) {
645664
return nil
646665
}
647666

@@ -661,14 +680,14 @@ func getRootNodeFromAny(model any) *yaml.Node {
661680

662681
if navigable, ok := model.(navigableNoder); ok {
663682
inner, err := navigable.GetNavigableNode()
664-
if err == nil && inner != nil {
683+
if err == nil && !isNilAny(inner) {
665684
// Recursively try to get root node from the inner value
666685
return getRootNodeFromAny(inner)
667686
}
668687
}
669688

670689
// Try to get core model and extract root node from there
671-
if core := getCoreModelFromAny(model); core != nil {
690+
if core := getCoreModelFromAny(model); !isNilAny(core) {
672691
if getter, ok := core.(rootNodeGetter); ok {
673692
return getter.GetRootNode()
674693
}

0 commit comments

Comments
 (0)