starlight-openapi
starlight-openapi copied to clipboard
Schema with recursion hangs building
Describe the bug
When I try to build a schema which includes recursion the process hangs.
To Reproduce
Build the following schema:
{
"openapi": "3.1.0",
"info": {
"title": "Performance",
"version": "1.0"
},
"paths": {
"/treesversions": {
"put": {
"description": "Creates a new tree version.",
"operationId": "PutTreeVersion",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PutTreeVersionRequestContent"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "PutTreeVersion 200 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PutTreeVersionResponseContent"
}
}
}
},
"400": {
"description": "ValidationException 400 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ValidationExceptionResponseContent"
}
}
}
},
"401": {
"description": "UnauthorizedError 401 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedErrorResponseContent"
}
}
}
},
"403": {
"description": "ForbiddenError 403 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ForbiddenErrorResponseContent"
}
}
}
},
"409": {
"description": "ConflictError 409 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ConflictErrorResponseContent"
}
}
}
},
"500": {
"description": "InternalServerError 500 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InternalServerErrorResponseContent"
}
}
}
},
"503": {
"description": "ServiceUnavailableError 503 response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent"
}
}
}
}
},
"x-amazon-apigateway-integration": {
"type": "aws_proxy",
"httpMethod": "PUT",
"uri": ""
}
}
}
},
"components": {
"schemas": {
"ConflictErrorResponseContent": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
]
},
"ForbiddenErrorResponseContent": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
]
},
"InternalServerErrorResponseContent": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
]
},
"PutTreeVersionRequestContent": {
"type": "object",
"properties": {
"treeVersionId": {
"type": "string",
"pattern": "^[A-Za-z0-9-]+$",
"description": "Tree version unique ID."
},
"version": {
"type": "string",
"description": "Tree version."
},
"treeId": {
"type": "string",
"pattern": "^[A-Za-z0-9-]+$",
"description": "Tree unique ID."
},
"treeVersionName": {
"type": "string",
"description": "Tree version name."
},
"startDate": {
"type": "number",
"description": "Tree version start date."
},
"otherNodeId": {
"type": "string",
"pattern": "^[A-Za-z0-9-]+$",
"description": "Tree version other node unique ID."
},
"nodes": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TreeNodeInput"
},
"description": "Tree nodes."
}
},
"required": [
"nodes",
"otherNodeId",
"startDate",
"treeId",
"treeVersionId",
"version"
]
},
"PutTreeVersionResponseContent": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"description": "Tree version creation was successful."
}
}
},
"ServiceUnavailableErrorResponseContent": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
]
},
"TreeNodeInput": {
"type": "object",
"properties": {
"treeNodeId": {
"type": "string",
"pattern": "^[A-Za-z0-9-]+$",
"description": "Tree node unique ID."
},
"treeNodeName": {
"type": "string",
"description": "Tree node name."
},
"nodes": {
"type": "array",
"items": {
"$ref": "#/components/schemas/TreeNodeInput"
},
"description": "Tree nodes."
},
"properties": {
"$ref": "#/components/schemas/TreeNodePropertiesInput"
}
},
"required": [
"treeNodeId"
]
},
"TreeNodePropertiesInput": {
"type": "object",
"properties": {
"investmentType": {
"type": "string",
"description": "Tree node investment type."
}
}
},
"UnauthorizedErrorResponseContent": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
},
"required": [
"message"
]
},
"ValidationExceptionField": {
"type": "object",
"description": "Describes one specific validation failure for an input member.",
"properties": {
"path": {
"type": "string",
"description": "A JSONPointer expression to the structure member whose value failed to satisfy the modeled constraints."
},
"message": {
"type": "string",
"description": "A detailed description of the validation failure."
}
},
"required": [
"message",
"path"
]
},
"ValidationExceptionResponseContent": {
"type": "object",
"description": "A standard error for input validation failures.\nThis should be thrown by services when a member of the input structure\nfalls outside of the modeled or documented constraints.",
"properties": {
"message": {
"type": "string",
"description": "A summary of the validation failure."
},
"fieldList": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ValidationExceptionField"
},
"description": "A list of specific failures encountered while validating the input.\nA member can appear in this list more than once if it failed to satisfy multiple constraints."
}
},
"required": [
"message"
]
}
},
"securitySchemes": {
"aws.auth.sigv4": {
"type": "apiKey",
"description": "AWS Signature Version 4 authentication",
"name": "Authorization",
"in": "header",
"x-amazon-apigateway-authtype": "awsSigv4"
}
}
},
"security": [
{
"aws.auth.sigv4": []
}
]
}
The component TreeNodeInput has a child nodes which is an array of the same type.
Expected behavior
Maybe it'd make sense to detect the recursion and stop generating additional levels, so the process doesn't get into infinite recursion.
How often does this bug happen?
Every time
System Info
Version: 0.6.4
Additional Context
No response
Thanks for the feedback 🙌
If possible, that would indeed be a nice enhancement. I remember that the parser we use has various options regarding circular references, so may need to play around with that and see if some of them could help :thumbsup:
I'm trying to include a really big OpenAPI spec file with this plugin and was running into #31 when building a static site or visiting the operations with the offending components in the dev server. After some investigation, it seems the memory issue was just a consequence of this recursion issue in this case.
Sharing my test results in the hope it helps someone:
-
Direct recursion like simple trees will be handled correctly, the generator will even note it in the generated html, e.g.: a category component with a reference to it's parent category.
components: schemas: Category: type: object required: - id - name properties: id: type: integer format: int64 name: type: string parent: $ref: '#/components/schemas/Category' -
Indirect recursion of any depth will not be detected and will cause the infinite loop, e.g.: a category component with a list of posts, each post having a reference to it's own category.
components: schemas: Category: type: object required: - id - name properties: id: type: integer format: int64 name: type: string posts: $ref: '#/components/schemas/Posts' Post: type: object required: - id - name properties: id: type: integer format: int64 name: type: string text: type: string category: $ref: '#/components/schemas/Category' Posts: type: array maxItems: 100 items: $ref: '#/components/schemas/Post'
Complete buggy example:
openapi: 3.1.0
info:
title: Test Recursion
description: Example of the recursion issue from starlight-openapi.
version: 1.0.0
servers:
- url: 'http://localhost'
paths:
/categories:
get:
summary: List all categories
operationId: listCategories
parameters:
- name: limit
in: query
description: How many categories to return at one time (max 100)
schema:
type: integer
maximum: 50
format: int32
nullable: true
- name: offset
in: query
description: The number of categories to skip before starting to collect the result set
required: false
schema:
type: integer
format: int32
responses:
'200':
description: A paged array of categories
content:
application/json:
schema:
$ref: '#/components/schemas/Categories'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/posts:
get:
summary: List all posts
operationId: listPosts
parameters:
- name: limit
in: query
description: How many posts to return at one time (max 100)
schema:
type: integer
maximum: 50
format: int32
nullable: true
- name: offset
in: query
description: The number of posts to skip before starting to collect the result set
required: false
schema:
type: integer
format: int32
responses:
'200':
description: A paged array of posts
content:
application/json:
schema:
$ref: '#/components/schemas/Posts'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Category:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
posts:
$ref: '#/components/schemas/Posts'
Categories:
type: array
maxItems: 100
items:
$ref: '#/components/schemas/Category'
Post:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
text:
type: string
category:
$ref: '#/components/schemas/Category'
Posts:
type: array
maxItems: 100
items:
$ref: '#/components/schemas/Post'
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string