From 674c21525bdba55103c4f68135beb94bdf0a3adb Mon Sep 17 00:00:00 2001 From: rifkir23 Date: Thu, 3 Sep 2026 19:32:48 +0700 Subject: [PATCH] fix: prevent nil embedded Reference panic on sibling operations when a $ref fails to build (#616) --- datamodel/low/v3/path_item.go | 8 +++++++ datamodel/low/v3/path_item_test.go | 38 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/datamodel/low/v3/path_item.go b/datamodel/low/v3/path_item.go index 5d7146aac..5146a7443 100644 --- a/datamodel/low/v3/path_item.go +++ b/datamodel/low/v3/path_item.go @@ -318,6 +318,10 @@ func (p *PathItem) Build(ctx context.Context, keyNode, root *yaml.Node, idx *ind if err := low.BuildModel(pathNode, &op); err != nil { return err } + // Initialize the embedded reference up front so the operation is safe to + // inspect (e.g. IsReference()) even if a sibling operation's Build fails + // and this one is never built. See issue #616. + op.Reference = &op.reference opRef := low.NodeReference[*Operation]{ Value: &op, @@ -374,6 +378,10 @@ func (p *PathItem) Build(ctx context.Context, keyNode, root *yaml.Node, idx *ind if err := low.BuildModel(opValueNode, &addOp); err != nil { return err } + // Initialize the embedded reference up front so the operation + // is safe to inspect even if a sibling operation's Build fails + // and this one is never built. See issue #616. + addOp.Reference = &addOp.reference addOpRef := low.NodeReference[*Operation]{ Value: &addOp, diff --git a/datamodel/low/v3/path_item_test.go b/datamodel/low/v3/path_item_test.go index fdf6ff2ba..80096df8e 100644 --- a/datamodel/low/v3/path_item_test.go +++ b/datamodel/low/v3/path_item_test.go @@ -392,3 +392,41 @@ func TestResolveOperationReference_EmptyTagNode(t *testing.T) { assert.Equal(t, "from-empty-tag-node", resolvedNode.Content[1].Value) assert.NotNil(t, foundCtx.Value(index.FoundIndexKey)) } + +// TestPathItem_Build_SiblingOperationSafeAfterRefError reproduces issue #616: +// when one operation in a path item fails to build (here, a dangling $ref), +// the sibling operations must still be safe to inspect. Previously they were +// returned with a nil embedded *low.Reference, so IsReference() panicked with a +// nil pointer dereference. +func TestPathItem_Build_SiblingOperationSafeAfterRefError(t *testing.T) { + yml := `get: + responses: + '200': + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/Nope' +post: + responses: + '200': + description: ok` + + var idxNode yaml.Node + _ = yaml.Unmarshal([]byte(yml), &idxNode) + idx := index.NewSpecIndex(&idxNode) + + var n PathItem + _ = low.BuildModel(idxNode.Content[0], &n) + err := n.Build(context.Background(), nil, idxNode.Content[0], idx) + + // the dangling $ref is expected to surface as an error... + assert.Error(t, err) + + // ...but the sibling POST operation must still be safe to inspect: its + // embedded reference is initialized, so IsReference() does not panic. + assert.NotNil(t, n.Post.Value) + assert.NotPanics(t, func() { + assert.False(t, n.Post.Value.IsReference()) + }) +}