Repository: getkin/kin-openapi
Pull request #310 (2021-02-28 09:24:16)

Prevent infinite loop while loading openapi spec with recursive references
Try to fix https://github.com/getkin/kin-openapi/issues/309


Patch:
diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go
index 1005dfc..ed2b052 100644
--- a/openapi3/swagger_loader.go
+++ b/openapi3/swagger_loader.go
@@ -34,7 +34,8 @@ type SwaggerLoader struct {
 
 	Context context.Context
 
-	visitedFiles map[string]struct{}
+	visitedFiles    map[string]struct{}
+	visitedSwaggers map[string]*Swagger
 
 	visitedHeader         map[*Header]struct{}
 	visitedParameter      map[*Parameter]struct{}
@@ -53,6 +54,7 @@ func NewSwaggerLoader() *SwaggerLoader {
 
 func (swaggerLoader *SwaggerLoader) reset() {
 	swaggerLoader.visitedFiles = make(map[string]struct{})
+	swaggerLoader.visitedSwaggers = make(map[string]*Swagger)
 }
 
 // LoadSwaggerFromURI loads a spec from a remote URL
@@ -170,11 +172,22 @@ func (swaggerLoader *SwaggerLoader) LoadSwaggerFromDataWithPath(data []byte, pat
 }
 
 func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataWithPathInternal(data []byte, path *url.URL) (*Swagger, error) {
+	visited, ok := swaggerLoader.visitedSwaggers[path.String()]
+	if ok {
+		return visited, nil
+	}
+
 	swagger := &Swagger{}
+	swaggerLoader.visitedSwaggers[path.String()] = swagger
+
 	if err := yaml.Unmarshal(data, swagger); err != nil {
 		return nil, err
 	}
-	return swagger, swaggerLoader.ResolveRefsIn(swagger, path)
+	if err := swaggerLoader.ResolveRefsIn(swagger, path); err != nil {
+		return nil, err
+	}
+
+	return swagger, nil
 }
 
 // ResolveRefsIn expands references if for instance spec was just unmarshalled


Test patch:
diff --git a/openapi3/swagger_loader_recursive_ref_test.go b/openapi3/swagger_loader_recursive_ref_test.go
new file mode 100644
index 0000000..45842d8
--- /dev/null
+++ b/openapi3/swagger_loader_recursive_ref_test.go
@@ -0,0 +1,17 @@
+package openapi3
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestLoaderSupportsRecursiveReference(t *testing.T) {
+	loader := NewSwaggerLoader()
+	loader.IsExternalRefsAllowed = true
+	doc, err := loader.LoadSwaggerFromFile("testdata/recursiveRef/openapi.yml")
+	require.NoError(t, err)
+	require.NotNil(t, doc)
+	require.NoError(t, doc.Validate(loader.Context))
+	require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo"].Value.Properties["bar"].Value.Items.Value.Example)
+}
diff --git a/openapi3/testdata/recursiveRef/components/Bar.yml b/openapi3/testdata/recursiveRef/components/Bar.yml
new file mode 100644
index 0000000..cc59fc2
--- /dev/null
+++ b/openapi3/testdata/recursiveRef/components/Bar.yml
@@ -0,0 +1,2 @@
+type: string
+example: bar
diff --git a/openapi3/testdata/recursiveRef/components/Foo.yml b/openapi3/testdata/recursiveRef/components/Foo.yml
new file mode 100644
index 0000000..0c08992
--- /dev/null
+++ b/openapi3/testdata/recursiveRef/components/Foo.yml
@@ -0,0 +1,6 @@
+type: object
+properties:
+  bar:
+    type: array
+    items:
+      $ref: ../openapi.yml#/components/schemas/Bar
diff --git a/openapi3/testdata/recursiveRef/openapi.yml b/openapi3/testdata/recursiveRef/openapi.yml
new file mode 100644
index 0000000..5dfcfbf
--- /dev/null
+++ b/openapi3/testdata/recursiveRef/openapi.yml
@@ -0,0 +1,13 @@
+openapi: "3.0.3"
+info:
+  title: Recursive refs example
+  version: "1.0"
+paths:
+  /foo:
+    $ref: ./paths/foo.yml
+components:
+  schemas:
+    Foo:
+      $ref: ./components/Foo.yml
+    Bar:
+      $ref: ./components/Bar.yml
diff --git a/openapi3/testdata/recursiveRef/paths/foo.yml b/openapi3/testdata/recursiveRef/paths/foo.yml
new file mode 100644
index 0000000..dd6c15d
--- /dev/null
+++ b/openapi3/testdata/recursiveRef/paths/foo.yml
@@ -0,0 +1,11 @@
+get:
+  responses:
+    "200":
+      description: OK
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              foo:
+                $ref: ../openapi.yml#/components/schemas/Foo