relay: [modern] Relay Compiler error: RangeError: Maximum call stack size exceeded

I previously wrote a comment in the thread #1660 about an error I am receiving when trying to build with the relay compiler. I have managed to work out why I am receiving an error, and I think it may be a bug with the compiler.

Take the following schema which has been extended / modified from the todo-modern relay example (the + is what I have added to the todo-modern schema for demo purposes):

+ input AddUserInput {
+ 	username: String!
+ 	password: String!
+ 	todo: AddTodoInput
+ }

  input AddTodoInput {
    text: String!
+   user: AddUserInput
    clientMutationId: String
  }

+ type AddUserPayload {
+   changedUser: User
+   clientMutationId: String
+ }

  type Mutation {
+   addUser(input: AddUserInput!): AddUserPayload
    changeTodoStatus(input: ChangeTodoStatusInput!): ChangeTodoStatusPayload
    markAllTodos(input: MarkAllTodosInput!): MarkAllTodosPayload
    removeCompletedTodos(input: RemoveCompletedTodosInput!): RemoveCompletedTodosPayload
    removeTodo(input: RemoveTodoInput!): RemoveTodoPayload
    renameTodo(input: RenameTodoInput!): RenameTodoPayload
  }

Notice the dependancy of AddTodoInput on type AddUserInput, but also the dependency of AddUserInput on type AddTodoInput.

When I run the relay compiler, I get the error RangeError: Maximum call stack size exceeded and the compiler immediately quits its operation and does not write any _generated_ queries or mutations.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 3
  • Comments: 40 (11 by maintainers)

Most upvoted comments

Ok tried that and it’s definitely not the same bug, as my patch was for babel-plugin-relay which is only needed for classic/compat mode.

I’ll be doing some work to clean up some patches I have for Relay soon, if time permits I’ll see if I can easily figure out this issue, but no promises.

Here’s a minimum example that reproduces the error: https://github.com/kguller/W3.harness.

You can run

yarn run schema
yarn run relay 

to receive

Writing default
Error writing modules:
RangeError: Maximum call stack size exceeded
Unchanged: 0 files
Written default in 0.37s
Done in 1.79s.

The working theory is that the recursive input type TesttestReferencesTestReference (definition) causes the stack size to exceed. It’s used as an argument here for example.

As @mjmahone, my understanding is that this should be supported by the Relay Compiler.

I’ve went ahead and reduced the given reproduction to the minimum. Please see the following repository: https://github.com/schickling/relay-compiler-bug 🐛

I can confirm that the problem seems to be recursive input types like in this example:

input CreateTestInput {
  testField: String!
  test: CreateTestInput
  clientMutationId: String!
}

@josephsavona @alloy @leebyron any thoughts on this?

P.S. I wanted to create a PR with a failing test case as @josephsavona suggested, however the testing setup seems to be a bit overwhelming so I created this minimal repo instead.

As discussed in #2142, this has been fixed in master with 6d49a52050069dd45f422fdb054092b1bbdda9b8.

@patrick-samy you either have to manually remove the unneeded recursive definitions from your schema temporarily or you can follow the work around I am using that I mention here in the PR https://github.com/facebook/relay/pull/2142#issuecomment-345327113

In case anyone in this thread is interested, there is a configuration option for the RelayFileWriter called inputFieldWhiteListForFlow. Any field names in this list will not have flow types generated for them (this is the part of the compiler that is crashing as it tries to generate flow types for objects that have recursive definitions in the schema). So white listing the field names that have recursive definitions solves the problem but results in less specific flow types.

I modified my local version of the compiler to first compute the strongly connected components in all mutation input definitions and add all the field names that appeared in a strongly connected component to the white list. Now the flow type is generated as specifically as possible, excluding those types that point back to themselves.

I am going to include this fix in my pull request with the failed test case and see if it is an acceptable solution because I am not entirely sure of the original intended use of the white list and I don’t think it is available as an external option to the compiler so there isn’t a comparable work around.

The recursion that is exceeding the call stack seems to be here https://github.com/facebook/relay/blob/b0fe7f37c06745dfc1f32f17ad5242754acc9209/packages/relay-compiler/core/RelayFlowGenerator.js#L452 I haven’t got around to making a failing test case yet for the RelayFlowGenerator, but my first impression is that there is going to be some way to detect that the generator has made a full circle in generating variable types when it encounters recursive inputs.

As a side note, I am also using graphcool and I have ironed out a few recursive definitions in my schema that only arose because of the lack of support of interfaces. (e.g. when I intend a file to be related to one and only one of three types but the graphcool generated schema assumes I might need a reference to all three types in every input which creates the recursive definition) Nonetheless, it seems at first glance that the compiler should be able to handle this case for when it is needed

@alloy We’ve been working on work arounds the past week and haven’t had anything conclusive until yesterday. I wasn’t able to identify if in fact the recursive inputs were the problem until this last change worked by removing a reference back to the recursive input 2-3 recursive steps down. I worked out for my teams app that 3 levels of recursive calls would be deep enough to support our routines. @marktani helped us realize that if we could halt the recursive pointers down the chain, we might get it to compile. I have to note that the work around below is not thoroughly tested or valid for most systems, it’s just to prove the concept.

The work around with marginal success is as follows:

Work Around: Alter Schema File 2 Levels Down Step 1) Consider an example mutation definition that includes the recursive input “testReferences” (input definition: TesttestReferencesTestReference).

input CreateTest { testField: String! testReferencesIds: [ID!] testReferences: [TesttestReferencesTestReference!] }

Step 2) The input definition TesttestReferencesTestReference is where the recursive loop begins. It includes a field named “tests” that is an input type of TestReferencetestsTest. The input definition TestReferencetestsTest has a field named “testReferences” that points back to TesttestReferencesTestReference input type and this is where we believe the recursion goes into an infinite loop.

input TesttestReferencesTestReference { testsIds: [ID!] tests: [TestReferencetestsTest!] } input TestReferencetestsTest { testField: String! testReferencesIds: [ID!] testReferences: [TesttestReferencesTestReference!] <-- POINTS BACK TO CREATE THE LOOP }

Step 3) We tried to flatten this out to prevent looping by going down 2-3 levels of recursion pointers and removing the reference as follows (these are in order of readability and not compilation order):

input CreateTest { testField: String! testReferencesIds: [ID!] testReferences: [TesttestReferencesTestReference!] } input TesttestReferencesTestReference { testsIds: [ID!] tests: [TestReferencetestsTest!] } input TestReferencetestsTest { testField: String! testReferencesIds: [ID!] testReferences: [TesttestReferencesTestReferenceEnd!] <-- POINTS TO A NEW TYPE } input TesttestReferencesTestReferenceEnd { testsIds: [ID!] tests: [TestReferencetestsTestEnd!] } input TestReferencetestsTestEnd { testField: String! testReferencesIds: [ID!] // REMOVED REFERENCE POINT }

Summary: This ends the recursive call and avoids any stack error. The mutation looks like it works, but I need to do more work to make sure there aren’t other status issues with the execution of these mutations. I think our previous attempts went too far down the stack and still ran into trouble. This scenario seemed to have worked and is based off the test environment provided by @schickling.

I just hope that this input opens up a path to address a need for a change in the compiler to only support recursive calls down to a particular level (and possibly allow a parameter to override the default and set a new level). The compiler would then stop traversing calls and create an end input/type of some sort like we did in the example below that does not have a recursive call and ends the chain. Again, a lot of assumptions and lack of insight into the compiler on my part. The work around above is highly error prone and messing with the schema of course is impractical on larger systems. Vendors could support locking out their schema recursive call levels at a certain number, so I guess either side could manage this, compiler vs. schema provider. Hope this is of help.

@kguller I’m using graph.cool as well, seems like a lot of people have same problem on that platform. Coincidence?

The real problem is that I’m really blocked with my work and there is no workaround solution 😦

+1 as well. I reported this issue over to @marktani with GraphCool who helped verify the problem and get it reported. Thank you for your help!

Just wanted to add that we tried to flatten out the recursive structure yesterday to work around the compiler error without success. Our team confirmed with GraphCool that the complexity multiplies as you attempt to make changes to the schema file, so it is quite a challenge. If anyone has had any success with a temporary work around, we’d love to hear about it. Unfortunately, our production project is at a standstill, so we are looking for any alternatives at this point.

Unfortunately I hit the same problem as well

mutation createCompanyAssignmentMutation($input: CreateCompanyAssignmentInput!) { createCompanyAssignment(input: $input) { companyAssignment { role company { id companyId companyName } user { id firstname lastname email createdBy { id } } } } }

@benjaminogles - Thanks, the workaround works like a charm! You saved me a painful weekend of work.

+1, Experiencing this issue aswell - mine is related to the same issue as @marktani has described. 😕

@benjaminogles , a hero indeed! Great work, thanks! Can also confirm that the workaround works great

Hey @chrish-21, it’s correct that changes in the way the Relay API is generated on Graphcool uncovered this problem.

However, the fact that the Relay compiler is not handling recursive input types correctly is still unexpected and should be considered a bug.

I was able to get the compiler to finish without exceeding the call stack by passing in a list of parent types when generating the mutation variable types and filtering parents out of the recursive map. But it doesn’t look like an ideal solution when multiple types are all referencing each other because you end up with a mutation where at different levels, depending on the type’s context, nested mutations are suddenly disallowed and the generated mutation definition is huge.

I guess I will try and make a good failing test case for the RelayFlowGenerator and then see what other solutions can be thought up. I think it would be great if the compiler could mark a mutation input as recursive at a certain point and then allow any depth of nested mutation as long as it repeated the recursive definition but it might be better to only generate the mutation to a predefined depth like @kguller suggested