Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

GraphQL query throws 'QueryKeyMaker cannot handle circular query structures' with simple React Native setup #2442

Closed
sammccord opened this issue Oct 1, 2018 · 34 comments
Assignees

Comments

@sammccord
Copy link

sammccord commented Oct 1, 2018

I spun up a new expo app with expo init, added requisite React/GraphQL dependencies, and was greeted with this fun, unshakable error:

QueryKeyMaker cannot handle circular query structures

whenever any <Query /> component is mounted.

Intended outcome:

I was attempting to query a public graphql endpoint from a simple react-native app, and see either data or a network error with the following dependencies.

package.json dependencies

"dependencies": {
    "apollo-boost": "^0.1.16",
    "apollo-utilities": "^1.0.21",
    "babel-plugin-inline-import-graphql-ast": "^2.4.1",
    "expo": "^30.0.1",
    "graphql": "^14.0.2",
    "graphql-tag": "^2.9.2",
    "react": "16.3.1",
    "react-apollo": "^2.2.3",
    "react-native": "https://github.com/expo/react-native/archive/sdk-30.0.0.tar.gz"
  }
// apolloClient.js
export default new ApolloClient({
  uri: 'https://fakerql.com/graphql'
})

// App.js
export default class App extends React.Component {
  render () {
    return (
      <ApolloProvider client={apolloClient}>
        <Query />
      </ApolloProvider>
    )
  }
}

// query.js
const TEST = gql`
  query me {
    me {
      id
    }
  }
`

export default class TestQuery extends React.Component {
  render () {
    return (
      <Query query={TEST}>
        {({ loading, error, data }) => {
          console.log(loading, error, data)
          return <View />
        }}
      </Query>
    )
  }
}

Actual outcome:

The app compiles, and throws immediately with the following stack trace:

QueryKeyMaker cannot handle circular query structures

How to reproduce the issue:

I've published a minimal example app that crashes with the same error when pointed at a public graphql endpoint. Here's the github repo, with instructions on how to reproduce locally.

It's occuring constantly on my OnePlus 6, with Android 9 Pie. Can't test on iOS (on linux), but would love to know if that happens there as well.

Version

  • apollo-client@2.4.2
  • react-apollo@2.2.3
@radiegtya
Copy link

same here

@FabioVerge
Copy link

FabioVerge commented Oct 1, 2018

I have the same, the error goes away when I enable Remote JS Debugging.

Version

  • react-native@0.57.0
  • react@16.5.0
  • react-apollo@2.2.3

@SMJ93
Copy link

SMJ93 commented Oct 1, 2018

@sammccord @radiegtya @FabioVerge We are experiencing the same issue too. Are you guys using yarn.lock or package.lock files?

Everything was working fine 3 days ago. Then today if I clean the repo (git clean -xfd) and install the dependencies it shows the error above.

Version

  • react-apollo@2.1.9
  • react-native@0.55.3
  • lerna@2.0.0

@sammccord
Copy link
Author

@SMJ93 I am, the minimum setup to reproduce uses this package-lock:

https://github.com/sammccord/QueryKeyMaker-issue-repo/blob/master/package-lock.json

@FabioVerge
Copy link

@SMJ93 We are using .lock files, but we had encountered a problem that required use to clean everything (including lock files), thereafter this problem arose.

@SMJ93
Copy link

SMJ93 commented Oct 1, 2018

@FabioVerge hmmmmm looks like that could be the issue. I had a similar issue a few months back where Metro released a breaking change as a minor release which broke everything.

It looks like a dependency in the tree has been updated which doesn't support React Native Android 🙁

@marcofaggian
Copy link

Same setup here.
Yes, it's working in iOS Emulator while in Android it's not.
Looks like apollo-cache-inmemory it's filing the Error.

@SMJ93
Copy link

SMJ93 commented Oct 1, 2018

@sammccord did you run npm install to create the lock file after Friday last week?

@SMJ93
Copy link

SMJ93 commented Oct 1, 2018

@kasstorr does it work for you in Android debug mode? Also where does it mention apollo-cache-inmemory?

@marcofaggian
Copy link

@Kasstorr does it work for you in Android debug mode? Also where does it mention apollo-cache-inmemory?

  1. Yeah, it's working with the debug option
  2. In the stack trace, in the expo app in my physical device, i tapped on the first report of the stack trace and opened a method of the apollo-cache-inmemory package (apollo-cache-inmemory/lib/bundle.umd.js:219).

@SMJ93
Copy link

SMJ93 commented Oct 1, 2018

@Kasstorr Good spot! That package was also published 3 days ago:

https://www.npmjs.com/package/apollo-cache-inmemory

@SMJ93
Copy link

SMJ93 commented Oct 1, 2018

Setting the version in our package.json fixed the issue:

 "apollo-cache-inmemory":"1.2.9",

@marcofaggian
Copy link

Setting the version in our package.json fixed the issue:

 "apollo-cache-inmemory":"1.2.9",

For me the error popped out moving to apollo-boost package, from single apollo-client, apollo-cache-inmemory and apollo-link-http. But going back to single packages solved the issue. My package.json's package versions:

    "apollo-cache-inmemory": "^1.2.10",
    "apollo-client": "^2.4.2",
    "apollo-link-http": "^1.5.5",

@SMJ93
Copy link

SMJ93 commented Oct 1, 2018

Nice 👍

Looks like the latest version of apollo-cache-inmemory is the issue. Hardcoding the version should fix it!

@benjamn benjamn self-assigned this Oct 2, 2018
benjamn added a commit to apollographql/apollo-client that referenced this issue Oct 3, 2018
This should help with github.com/apollographql/react-apollo/issues/2442,
as long as the known properties of query document nodes do not participate
in cycles.
@benjamn
Copy link
Member

benjamn commented Oct 3, 2018

Please try the latest version of apollo-cache-inmemory (1.3.3) when you have a chance, since I believe apollographql/apollo-client@db0ce68 will help with this issue:

npm install apollo-cache-inmemory@next

Specifically, the QueryKeyMaker still complains if it encounters circular query structures, but it's more careful now about which subfields it traverses, so it shouldn't get confused by stray metadata properties that happen to contain circular references.

As a side note, I was not actually able to reproduce the problem (though I very much appreciate the runnable reproduction!), so it would be very helpful if anyone can shed some light on the circular references in question. Are they present in the AST document returned by the graphql-js parser or the gql template tag? Or are they added at some later point in the React Native pipeline?

There's definitely more that we can do here to tolerate/prevent circular references; it's just a matter of diagnosing the underlying causes and weighing possible solutions.

@gabceb
Copy link

gabceb commented Oct 3, 2018

Cheers @benjamn. Just bumped into this on a new app that we have and I am now getting the error below (like most folks above crashes on Android but not on iOS). It looks like the issue comes from the facebook Map.js polyfill when we are trying to .get on the cache. When I move down to the latest version (1.2.10) I see the QueryKeyMaker circular issues but when I move to 1.3.3-beta5 and above including 1.3.3 it switches to this error:

image

@gabceb
Copy link

gabceb commented Oct 3, 2018

Like most other folks downgrading to 1.2.9 fixes the issue

@sammccord
Copy link
Author

@benjamn Thanks for looking into this. 1.3.3 still appears to throw for me, while locking to 1.2.9 works fine. I'm not convinced this has anything to do with the structures being queried since in the initial example I posted, it was throwing when only a primitive was requested in the query.

benjamn added a commit to apollographql/apollo-client that referenced this issue Oct 3, 2018
As explained here, with astonishing honesty:
https://github.com/facebook/react-native/blob/98a6f19d7c41e1d4fd3c04d07bb2c4e627f21724/Libraries/vendor/core/Map.js#L44-L50

As reported most recently here:
apollographql/react-apollo#2442 (comment)

For the record, I have previously complained that these polyfills are
simply buggy and should be replaced:
#3394 (comment)

While that is still true, this workaround should moot the debate. 🐮
benjamn added a commit to apollographql/apollo-client that referenced this issue Oct 3, 2018
As explained here, with astonishing honesty:
https://github.com/facebook/react-native/blob/98a6f19d7c41e1d4fd3c04d07bb2c4e627f21724/Libraries/vendor/core/Map.js#L44-L50

As reported most recently here:
apollographql/react-apollo#2442 (comment)

For the record, I have previously complained that these polyfills are
simply buggy and should be replaced:
#3394 (comment)

While that is still true, this workaround should moot the debate. 🐮
@benjamn
Copy link
Member

benjamn commented Oct 3, 2018

@gabceb While I still find it astonishing that the authors of the React Native Map and Set polyfills thought they could get away with simply not supporting non-extensible object keys, I've devised this (pretty clever, IMO) workaround to treat the symptoms: apollographql/apollo-client@833072e

Try it out in apollo-cache-inmemory@1.3.4 when you get a chance!

@benjamn
Copy link
Member

benjamn commented Oct 3, 2018

@sammccord Any chance you can connect a debugger to your repro app and set some breakpoints in the QueryKeyMaker code in question? Since I haven't been able to reproduce the problem myself, I'm hoping to find out which value is causing a cycle. If you look at the call stack, you should also be able to determine the sequence of properties that lead from the original DocumentNode to that object.

@gabceb
Copy link

gabceb commented Oct 3, 2018

@benjamn 1.3.4 puts me back on circular QueryKeyMaker. See the output for the value and cached vars

value:

{
  "selectionSet": {
    "selections": [
      {
        "directives": [],
        "arguments": [],
        "name": {
          "value": "timesheetId",
          "kind": "Name"
        },
        "kind": "Field"
      },
      {
        "directives": [],
        "arguments": [],
        "name": {
          "value": "employerId",
          "kind": "Name"
        },
        "kind": "Field"
      },
      {
        "directives": [],
        "arguments": [],
        "name": {
          "value": "weekEnding",
          "kind": "Name"
        },
        "kind": "Field"
      },
      {
        "selectionSet": {
          "selections": [
            {
              "directives": [],
              "arguments": [],
              "name": {
                "value": "fullName",
                "kind": "Name"
              },
              "kind": "Field"
            },
            {
              "directives": [],
              "arguments": [],
              "name": {
                "value": "avatarUrl",
                "kind": "Name"
              },
              "kind": "Field"
            },
            {
              "kind": "Field",
              "name": {
                "kind": "Name",
                "value": "__typename"
              }
            }
          ],
          "kind": "SelectionSet"
        },
        "directives": [],
        "arguments": [],
        "name": {
          "value": "contact",
          "kind": "Name"
        },
        "kind": "Field"
      },
      {
        "selectionSet": {
          "selections": [
            {
              "directives": [],
              "arguments": [],
              "name": {
                "value": "shiftId",
                "kind": "Name"
              },
              "kind": "Field"
            },
            {
              "directives": [],
              "arguments": [],
              "name": {
                "value": "forecastCharge",
                "kind": "Name"
              },
              "kind": "Field"
            },
            {
              "directives": [],
              "arguments": [],
              "name": {
                "value": "jobTitle",
                "kind": "Name"
              },
              "kind": "Field"
            },
            {
              "directives": [],
              "arguments": [],
              "name": {
                "value": "name",
                "kind": "Name"
              },
              "kind": "Field"
            },
            {
              "directives": [],
              "arguments": [],
              "name": {
                "value": "timeRange",
                "kind": "Name"
              },
              "kind": "Field"
            },
            {
              "directives": [],
              "arguments": [],
              "name": {
                "value": "startDate",
                "kind": "Name"
              },
              "kind": "Field"
            },
            {
              "directives": [],
              "arguments": [],
              "name": {
                "value": "endDate",
                "kind": "Name"
              },
              "kind": "Field"
            },
            {
              "selectionSet": {
                "selections": [
                  {
                    "directives": [],
                    "arguments": [],
                    "name": {
                      "value": "type",
                      "kind": "Name"
                    },
                    "kind": "Field"
                  },
                  {
                    "directives": [],
                    "arguments": [],
                    "name": {
                      "value": "date",
                      "kind": "Name"
                    },
                    "kind": "Field"
                  },
                  {
                    "directives": [],
                    "arguments": [],
                    "name": {
                      "value": "comment",
                      "kind": "Name"
                    },
                    "kind": "Field"
                  },
                  {
                    "selectionSet": {
                      "selections": [
                        {
                          "directives": [],
                          "arguments": [],
                          "name": {
                            "value": "fullName",
                            "kind": "Name"
                          },
                          "kind": "Field"
                        },
                        {
                          "directives": [],
                          "arguments": [],
                          "name": {
                            "value": "tesUserId",
                            "kind": "Name"
                          },
                          "kind": "Field"
                        },
                        {
                          "kind": "Field",
                          "name": {
                            "kind": "Name",
                            "value": "__typename"
                          }
                        }
                      ],
                      "kind": "SelectionSet"
                    },
                    "directives": [],
                    "arguments": [],
                    "name": {
                      "value": "user",
                      "kind": "Name"
                    },
                    "kind": "Field"
                  },
                  {
                    "kind": "Field",
                    "name": {
                      "kind": "Name",
                      "value": "__typename"
                    }
                  }
                ],
                "kind": "SelectionSet"
              },
              "directives": [],
              "arguments": [],
              "name": {
                "value": "events",
                "kind": "Name"
              },
              "kind": "Field"
            },
            {
              "kind": "Field",
              "name": {
                "kind": "Name",
                "value": "__typename"
              }
            }
          ],
          "kind": "SelectionSet"
        },
        "directives": [],
        "arguments": [],
        "name": {
          "value": "shifts",
          "kind": "Name"
        },
        "kind": "Field"
      },
      {
        "kind": "Field",
        "name": {
          "kind": "Name",
          "value": "__typename"
        }
      }
    ],
    "kind": "SelectionSet"
  },
  "directives": [],
  "arguments": [
    {
      "value": {
        "name": {
          "value": "employerId",
          "kind": "Name"
        },
        "kind": "Variable"
      },
      "name": {
        "value": "employerId",
        "kind": "Name"
      },
      "kind": "Argument"
    }
  ],
  "name": {
    "value": "listCurrentTimesheets",
    "kind": "Name"
  },
  "kind": "Field"
}

cached:

{}

@benjamn
Copy link
Member

benjamn commented Oct 3, 2018

@gabceb Thanks!

That expansion of value seems like it got cut off, though. If value is circular, that's because it contains itself at some point, so I'd like to know where that happens. So far it just looks like an ordinary tree.

What happens if you try JSON.stringify(value) in the debugger (it should throw an error about circular references)?

The cached value in this case is just the CIRCULAR sentinel object, so it's no surprise that it looks like an empty object (because it is).

@gabceb
Copy link

gabceb commented Oct 3, 2018

@benjamn value updated on the comment above. I stringified right inside the if statement when the circular reference is detected

@benjamn
Copy link
Member

benjamn commented Oct 3, 2018

Interesting! So there don't appear to be any circular references that are visible to JSON.stringify.

A couple more questions:

  1. Are you running this on a device, or a simulator? (Assuming Android and not iOS, based on your previous comment.)

  2. While you're stopped on the throw line, if you check the stack trace in the devtools, I'm wondering what the last few object keys in the trace were. If you can find this code on the stack, the key should tell you how we got to the current value from its parent object. Hopefully, if that key is not a key that we expected, we can just always ignore it, and the problem will be solved.

@gabceb
Copy link

gabceb commented Oct 3, 2018

Re: your first question, I am running on a Android simulator on dev which means the js will actually be bundled on the machine using node but executed on the device using whatever js runtime. Yesterday I did see the non-extensible issue on a device running a prod build but have not tried on this new version. Looking into your second question.

@gabceb
Copy link

gabceb commented Oct 3, 2018

@benjamn See https://gist.github.com/gabceb/7a33b0769d05cfffa1a4941d07c81e91 for all the key variables on a console.log in this piece of code:

PerQueryKeyMaker.prototype.lookupObject = function (object) {
            var _this = this;
            var keys = safeSortedKeys(object);
            var values = keys.map(function (key) {
                console.log("KEY", key);
                return _this.lookupAny(object[key]);
            });
            return this.cacheKeyRoot.lookup(Object.getPrototypeOf(object), this.cacheKeyRoot.lookupArray(keys), this.cacheKeyRoot.lookupArray(values));
        };

Note that there is a single Query element on the codebase (I had more but removed them to make debugging easier)

@gabmichels
Copy link

got rid of apollo-boost and moved to single packages although the apollo-cache-inmemory 1.2.10 still crashes, so downgraded to 1.2.9

@benjamn
Copy link
Member

benjamn commented Oct 4, 2018

@thegreatgabsby This issue refers to code added in apollo-cache-inmemory@1.3.0 and later, so any difference between 1.2.10 and 1.2.9 should be discussed in a different issue (which you should feel free to open).

As a more general note, apollo-cache-inmemory@1.3.0 added almost 100 commits over five months of work (see apollographql/apollo-client#3394), so the observation that downgrading to 1.2.x fixes the problem is not as helpful as it would usually be, since the problem has so many possible places to hide. While downgrading is the recommended workaround for this problem, it's not the solution. If you'd like to help solve this problem, so that you (and everyone else) can safely reap the substantial performance improvements in 1.3.x, please feel free to try the reproduction and see if you can figure out what's going wrong in the QueryKeyMaker on Android.

@Vitiell0
Copy link

Vitiell0 commented Oct 4, 2018

We were running into this issue as everyone else but downgrading to apollo-cache-inmemory@1.2.9 in our package.json was not working.

Found out this was caused because we were still using apollo-client-preset which uses apollo-cache-inmemory@1.3.0.

apollo-client-preset is deprecated so we removed it and it's now working using apollo-cache-inmemory@1.2.9 in package.json

@carllippert
Copy link

carllippert commented Oct 4, 2018

@benjamn I upped to 1.3.4 to try and fix the circular query structures issue. No luck. Anything I can give you to help diagnose?

Notes:

Happens on device in release. Happens on android emulator. Does not happen on android emulator with debugging on.

@benjamn
Copy link
Member

benjamn commented Oct 4, 2018

Exciting news! This issue turned out to be due to some unfortunate bugs in the React Native Map polyfill, which I have submitted a PR to fix: facebook/react-native#21492. This is my first React Native PR, but I have complete confidence they'll review and merge it quickly.

In other words, this is entirely not my fault, and instead the result of folks rolling their own implementation of the ECMAScript Map container, even though there are multiple well-tested alternatives already available. I want to be snarky about this decision, but I also think the React Native Map polyfill has some interesting advantages over other polyfills, so I really hope the outcome is that it gets fixed.

If I'm being honest: I'm not feeling so great right now about the profession we've chosen. How does software even work, like, ever, at all?

@gabceb
Copy link

gabceb commented Oct 4, 2018

Who says software works? I guess you didnt know is just a bunch of leprechauns showing things on screens 😁

Awesome work @benjamn 👏🏻

larixer added a commit to sysgears/apollo-universal-starter-kit that referenced this issue Oct 5, 2018
@benjamn
Copy link
Member

benjamn commented Oct 5, 2018

Ok, if you update to apollo-cache-inmemory@1.3.5, this problem should be fixed!

@SMJ93
Copy link

SMJ93 commented Oct 6, 2018

Thanks @benjamn great work 👏

larixer added a commit to sysgears/apollo-universal-starter-kit that referenced this issue Oct 23, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants