question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Updating with missing data makes query fail silently

See original GitHub issue

I have a typical usecase where a mutation update inserts a new element in a list. Very similar to the standard example from the docs.

But the query is of course much more complicated. In my case I am creating a rating that has two 1-1 relation to a movie and a user object. The update function has to update both. The issue was when updating the user object by inserting the rating with the corresponding movie id on the users rating list. All that works great so far. But the issue was that the query where the users ratings are requested was reading more information from the movie object than what was already loaded for some movies. Now the query just returned null. I took me about a day to figure out that the one missing field was the issue.

To illustrate lets say this is the query on the user object that gets the ratings:

query userRatings {
  currentUser {
    ratings {
      id
      movie {
        id
        title
        poster
      }
   }
  }
}

And this is the mutation

mutation createRating {
  newRating: createRating(value: 4) {
    id
    movie {
      id
      title
    }
  }
}

Now when another query had already loaded the movie poster into the cache all was well, but when the poster was missing the userRatings query simply returned null without any errors being shown. In reality it was quite hard to figure out what even had happened because everything seemed to work but on the user profile screen the data suddenly disappeared.

Intended outcome:

I would have expected that there would be an error message from the userRatings query and that just the missing data would have been null but the rest there. Basically how a query to the server would behave. It seems that client side cache updates don’t prduce errors in other queries.

Actual outcome:

Nothing indicated that there was any error in apollo, the data simply disappeared. I am on react native, and the data disappeared on a different screen, making this very hard to track down as I first needed to find out that the mutation even caused this and because it just happened for movies where the poster wasn’t already fetched by another query in the application.

Version

  • apollo-cache-inmemory: ^1.1.4
  • apollo-cache-persist: ^0.1.0
  • apollo-client: ^2.0.4

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:21
  • Comments:12 (3 by maintainers)

github_iconTop GitHub Comments

12reactions
mlcohencommented, Feb 6, 2018

We’re also running into this issue, and like @MrLoh and @maierson, it’s also been very difficult to debug.

In our particular scenario, we have one feature (feature A) making a GQL query, like this:

query workspace($workspaceId: String!) {
    workspace(id: $workspaceId) {
        id
        contract {
            id
            versions {
                id
                type
                status
            }
        }
    }
}

When a new workspace is created on our platform, a contract also gets created and is associated with the workspace; however, the contract starts off with no versions. When the above query is executed against our GQL server for a newly created workspace, we get a result that looks like this:

{
    "data": {
        "workspace": {
            "id": "00000000-0000-AAAA-0000-00000000000A",
           "contract": {
               "id": "00000000-0000-BBBB-0000-00000000000B",
               "versions": [],
               "__typename": "ContractType"
           },
            "__typename": "WorkspaceType"
        }
    }
}

So far, so good.

Elsewhere in the app, we have another feature (feature B) that makes a different GQL query that may include contract information:

query chat($workspaceId: String!, $eventId: ID!) {
    workspace(id: $workspaceId) {
            id
            chatroom {
                id
                event(id: $eventId) {
                    id
                    resource {
                        ... on ContractType {
                            id
                            versions {
                                id
                                type
                           }
                       }                             
                   }
                }
            }
        }
    }
}

Above, the query is fetching a chat event that has an associated resource. A resource can take on many different types, one of which is a contract type (union shown above for brevity). You’ll notice that in the first GQL query, it requests a contract’s versions having fields id, type, and status, but for the chat GQL query, it requests only a version’s id and type fields.

Alright, so here’s how things play out – brace for impact…

Eventually a new contract version is created, this will cause the system to send out a chat event to all users that are part of a workspace which the contract belongs to. The frontend web app that makes use of the apollo client will receive a message (via a websocket) notifying the app of a new chat event. Feature B in the web app will pick up on this notification and in turn make a call via the apollo client to get more chat event information. Once the graphql server passes back a response, the apollo client triggers all of the GQL queries associated with the workspace and contract, which in this scenario is feature A and B. In the case of feature B everything works. Nothing blows up. However, feature A does blow up, and when we look for errors we see nothing.

Like @MrLoh, the issues came down to the data passed back from the server and how apollo client reconciles it. Initially a contract’s versions field is null. Within apollo client, it has a query manager (QueryManager) that will get a query’s current result via its getCurrentQueryResult method. Within the getCurrentQueryResult method, the manager reads from the data store’s cache (this.dataStore.getCache().read). The cache’s read method will properly reconcile objects when they are null or an empty list. The cache simply ignores that object’s fields that a query wants. Where things go wrong is when two or more queries that want the same object but ask for different fields on that object. If a query asks for a field on an object that is undefined, the cache’s read method throws an error. That’s fine. The problem is that the query manager’s getCurrentQueryResult silently swallows the error with a try-catch block and simply executes a maybeDeepFreeze:

 public getCurrentQueryResult<T>(observableQuery: ObservableQuery<T>) {
    const { variables, query } = observableQuery.options;
    const lastResult = observableQuery.getLastResult();
    const { newData } = this.getQuery(observableQuery.queryId);
    if (newData) {
      return maybeDeepFreeze({ data: newData.result, partial: false });
    } else {
      try {
        // the query is brand new, so we read from the store to see if anything is there
        const data = this.dataStore.getCache().read({
          query,
          variables,
          previousResult: lastResult ? lastResult.data : undefined,
          optimistic: true,
        });

        return maybeDeepFreeze({ data, partial: false });
      } catch (e) {
        return maybeDeepFreeze({ data: {}, partial: true });
      }
    }
  }

(see https://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/QueryManager.ts#L969)

When digging in and examining the error, the error does include detailed information why the cache’s read failed. That would be super handy to know.

So, going back to our scenario, for feature A, its query wants a contract version’s id, type and status, but since feature B used a query that only gets a contract’s versions with fields id and type but no status, this causes the data store’s cache read method to throw an error, but one that we don’t see.

This took us a while to track down the root cause and really understand what was going wrong.

Given what we now know, I’d like to add on to @MrLoh and @maierson thoughts for what the intended outcome should be.

At minimum, there should be a way to configure apollo client so that instead of silently swallowing thrown read query errors, the client will instead loudly raises the error for dev’s to easily track down. Better yet, it would be really nice if there were a way to handle these type of read errors gracefully. For instance, the apollo client could provide some kind of hook to optionally handle read errors. When handling the error, queries that failed could be run again to fetch data each query expects.

Anyway, apologies for such a long comment. I figured it would be useful for anyone else who is going down this long, winding road. And thanks to @MrLoh for initially raising this issue 😊

Other details

  • apollo-client: ^2.2.2
  • apollo-cache-inmemory: ^1.1.7
  • apollo-link: ^1.0.7
  • react-apollo: ^2.0.4
6reactions
maiersoncommented, Feb 2, 2018

I am also running into this and it is very hard to debug. I’m on the web so I do get an error but it’s very cryptic and not really actionable without extensive digging.

Encountered a sub-selection on the query, but the store doesn't have an object reference.

I feel it would indeed be much more useful if the cache returned null values for fields that are not yet present instead of blowing up. It would at least be easier to track down. Or if not can we at least have a reference to the query and the object missing in the error message? Something like

The following query

query {
   loadItem {
     title
     description
     child {
       name
     }
   }
}

is requesting a "name" sub selection on the field "child" but the store doesn't have a child object reference.

This way at least there’s a way to know what data exactly is missing (has not previously been requested - ie the child field). In my case this happens when I deploy the app to the server where it loads legacy data. Everything works well on the client side. So even more complicated to track down.

Thanks

Read more comments on GitHub >

github_iconTop Results From Across the Web

Why SQL UPDATE FROM silently fails when you refer to fieds ...
1 Answer 1 ... You are missing a WHERE clause (that cannot be written as direct JOIN condition). The additional table usermigration has...
Read more >
Refetching queries in Apollo Client - Apollo GraphQL Docs
Refetching queries in Apollo Client. Apollo Client allows you to make local modifications to your GraphQL data by updating the cache, but sometimes...
Read more >
Silent Data truncation in SQL Server 2019 - SQLShack
We normally call it as silent truncation and occur when we try to insert string data (varchar, nvarchar, char, nchar) into more than...
Read more >
PostgreSQL batch inserts fail silent randomly
I have been using Kettle to import data from a SQL Server to PostgreSQL on AWS RDS and when I was enabling all...
Read more >
Trials and Tribulations Preventing Silent Data Loss - phData
This happens when the data pipeline seems to run perfectly, without any exceptions or other obvious indications of error but turns out to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found