Repository navigation
Improper handling of cyclical directive defs (spec issue) #4201
Copy link
Copy link
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.
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);
});
}
}
Reactions are currently unavailable
Activity
github-actions commentedon Mar 9, 2026
Hello, this issue has been inactive for 60 days, so we're marking it as stale. If you would like to continue this discussion, please comment within the next 30 days or we'll close the issue.
Fix 5 open issues: subscription memory leak, cyclical directives, dup…
github-actions commentedon Apr 8, 2026
Hello, as this issue has been inactive for 90 days, we're closing the issue. If you would like to resume the discussion, please create a new issue.
andimarek commentedon May 19, 2026
See #4385 for a fix