Skip to content

Improper handling of cyclical directive defs (spec issue) #4201

@rstata

Description

@rstata

Describe the bug

The spec prohibits the definitions of directives to reference each other directly or indirectly via applied directives. GraphQL Java (25) correctly detects the direct case, but overflows the stack during validation for the indirect case.

Diagnostics link

To Reproduce

import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;
import graphql.schema.idl.UnExecutableSchemaGenerator;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertThrows;

/**
 * Reproduction test for graphql-java circular directive dependency bug.
 *
 * When directives mutually reference each other via applied directives on their
 * arguments, UnExecutableSchemaGenerator.makeUnExecutableSchema() throws
 * StackOverflowError instead of detecting the cycle and reporting a proper error.
 *
 * NOTE: graphql-java correctly detects direct self-reference (A -> A) and throws
 * SchemaProblem, but fails to detect indirect cycles (A -> B -> A) or longer
 * cycles (A -> B -> C -> A), resulting in StackOverflowError.
 *
 * Dependencies: graphql-java, JUnit 5
 */
public class CircularDirectiveStackOverflowTest {

    @Test
    void selfReferencingDirectiveCorrectlyThrowsSchemaProblem() {
        // A directive that references itself via applied directive on its argument
        // graphql-java correctly detects this direct self-reference
        String sdl = """
            directive @recursive(depth: Int @recursive(depth: 0)) on FIELD_DEFINITION | ARGUMENT_DEFINITION

            type Query { field: String @recursive(depth: 5) }
            """;

        TypeDefinitionRegistry registry = new SchemaParser().parse(sdl);

        // This correctly throws SchemaProblem (not a bug)
        assertThrows(graphql.schema.idl.errors.SchemaProblem.class, () -> {
            UnExecutableSchemaGenerator.makeUnExecutableSchema(registry);
        });
    }

    @Test
    void mutualDirectiveReferenceCausesStackOverflowError() {
        // Two directives that reference each other via applied directives on arguments
        // This is an indirect cycle: @foo -> @bar -> @foo
        String sdl = """
            directive @foo(x: Int @bar(y: 1)) on FIELD_DEFINITION | ARGUMENT_DEFINITION
            directive @bar(y: Int @foo(x: 2)) on FIELD_DEFINITION | ARGUMENT_DEFINITION

            type Query { field: String @foo(x: 10) @bar(y: 20) }
            """;

        TypeDefinitionRegistry registry = new SchemaParser().parse(sdl);

        // BUG: This throws StackOverflowError instead of a proper validation error
        assertThrows(StackOverflowError.class, () -> {
            UnExecutableSchemaGenerator.makeUnExecutableSchema(registry);
        });
    }

    @Test
    void threeWayCircularDirectiveReferenceCausesStackOverflowError() {
        // Three directives forming a cycle: @dirA -> @dirB -> @dirC -> @dirA
        String sdl = """
            directive @dirA(x: Int @dirB(y: 1)) on FIELD_DEFINITION | ARGUMENT_DEFINITION
            directive @dirB(y: Int @dirC(z: 2)) on FIELD_DEFINITION | ARGUMENT_DEFINITION
            directive @dirC(z: Int @dirA(x: 3)) on FIELD_DEFINITION | ARGUMENT_DEFINITION

            type Query { field: String @dirA(x: 10) @dirB(y: 20) @dirC(z: 30) }
            """;

        TypeDefinitionRegistry registry = new SchemaParser().parse(sdl);

        // BUG: This throws StackOverflowError instead of a proper validation error
        assertThrows(StackOverflowError.class, () -> {
            UnExecutableSchemaGenerator.makeUnExecutableSchema(registry);
        });
    }
}

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions