Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schema with recursion hangs building #41

Closed
inpt333 opened this issue Jul 16, 2024 · 2 comments
Closed

Schema with recursion hangs building #41

inpt333 opened this issue Jul 16, 2024 · 2 comments
Labels
enhancement New feature or request

Comments

@inpt333
Copy link

inpt333 commented Jul 16, 2024

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

@HiDeoo
Copy link
Owner

HiDeoo commented Jul 16, 2024

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 👍

@HiDeoo HiDeoo added the enhancement New feature or request label Jul 16, 2024
@ribeirobreno
Copy link
Contributor

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

ribeirobreno added a commit to ribeirobreno/starlight-openapi that referenced this issue Sep 28, 2024
@HiDeoo HiDeoo closed this as completed Oct 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants