Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 7, 2026

GraphQL spec prohibits directives from referencing each other via applied directives. Direct self-reference (A → A) was detected correctly, but indirect cycles (A → B → A or A → B → C → A) caused StackOverflowError during schema generation.

Changes

  • SchemaTypeDirectivesChecker: Added DFS-based cycle detection that traverses directive dependencies via applied directives on arguments. Uses canonical cycle keys to report each cycle once.
  • DirectiveIllegalReferenceError: Added constructor for cycle path error messages showing the full cycle (e.g., foo -> bar -> foo).
  • Tests: Added unit and integration tests for two-way cycles, three-way cycles, and non-cyclic references.

Example

# Previously caused StackOverflowError, now throws SchemaProblem
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) }

Error message: 'foo' forms a directive cycle via: foo -> bar -> foo

Original prompt

This section details on the original issue you should resolve

<issue_title>Improper handling of cyclical directive defs (spec issue)</issue_title>
<issue_description>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);
        });
    }
}

</issue_description>

Comments on the Issue (you are @copilot in this section)


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI changed the title [WIP] Fix improper handling of cyclical directive definitions Fix detection of indirect cyclical directive references Jan 7, 2026
Copilot AI requested a review from andimarek January 7, 2026 07:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improper handling of cyclical directive defs (spec issue)

2 participants