Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updating with missing data makes query fail silently #2920

Closed
MrLoh opened this issue Jan 28, 2018 · 12 comments
Closed

Updating with missing data makes query fail silently #2920

MrLoh opened this issue Jan 28, 2018 · 12 comments

Comments

@MrLoh
Copy link

MrLoh commented Jan 28, 2018

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
@maierson
Copy link

maierson commented 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

@mlcohen
Copy link

mlcohen commented 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

@softhib
Copy link

softhib commented Feb 15, 2018

Ha ! It seems like I'm not the only one facing this missing data problem ! It's been 3 days since I tried to debug the following problem (only on iOS). And it is still not debugged...

I figured out that the problem comes from a specific screen but I don't exactly know what makes this specific data return null (in my case : userLogged from my queryUserLoggedInfos query) and how to resolve the issue.

In your case, It seems that comes from a missing field but how did you find it ?

Considering the following snippet :

render() {
if (this.props.queryUserLoggedInfos.networkStatus === 1) return null; 
const { userLogged } = this.props.queryUserLoggedInfos;
console.log("=========render userLogged==========");
console.log(userLogged);
console.log("====================================");

(...)

I got the following result on iOS :

[apollo-cache-persist] Restored cache of size 90659
=========render userLogged==========
{id: "5a2c769d4ef79d57e29c940a", pseudo: "sof", email: "sofiene@torepa.xyz", avatar: "ws5icnrjntmd4bbbvlay", tribu: {}, …}
====================================
=========render userLogged==========
{id: "5a2c769d4ef79d57e29c940a", pseudo: "sof", email: "sofiene@torepa.xyz", avatar: "ws5icnrjntmd4bbbvlay", tribu: {}, …}
====================================
remote-redux-devtools: Socket connection errors are being suppressed. 
This can be disabled by setting suppressConnectErrors to 'false'.
SocketProtocolError {name: "SocketProtocolError", message: "Socket hung up", code: 1006, stack: "SocketProtocolError: Socket hung up↵    at SCSocke…a/node_modules/expo/tools/hashAssetFiles:2316:42)"}
=========render userLogged==========
null
====================================
[apollo-cache-persist] Persisted cache of size 90595

So after the first two renders, the cache is filled but at the end of the third it becomes empty. So user is logged during few milliseconds and is logout just after... And this problem does not appear on Android...

On Android there only are two renders instead of three.

[apollo-cache-persist] Restored cache of size 35420
=========render userLogged==========
Object {
  "__typename": "User",
  "avatar": "muv3zrworf6bg2mcap9o",
  "email": "gexxxxbault@gmail.com",
  (...)
}
====================================
=========render userLogged==========
Object {
  "__typename": "User",
  "avatar": "muv3zrworf6bg2mcap9o",
  "email": "geraxxxxault@gmail.com",
  (...)
  },
}
====================================
[apollo-cache-persist] Persisted cache of size 33281

Really strange behavior...

Versions :

"apollo-cache-persist": "^0.1.1",
"apollo-client-preset": "^1.0.5",
"apollo-link": "^1.0.7",
"apollo-link-ws": "^1.0.4",
"apollo-utilities": "^1.0.4",

@martinek
Copy link

I have probably hit this same problem.

I have two queries called from two different screens. Both queries retrieve some combination of objects from server but one of them retrieves less attributes then the other one.

What I noticed is that the queries need to be called in specific order for this bug to appear.

Lets say I have two screens with two queries.
Screen A requests model 1 with attributes X, Y and Z.
Screen B requests same model 1 only with attributes X and Y.

Now following happens:
Open screen A - query A executes and everything loads as it should, data is OK, contain X, Y, Z
Open screen B - query B executes and everything loads as it should, data is OK and only contain X, Y
Open screen A - query A is not executed again (data from cache), data is not OK, only contain X and Y

Another scenario:
Open screen B - query B executes and everything loads as it should, data is OK, contain X and Y
Open screen A - query A executes, everything is OK, contain X, Y, Z
Open screen B again - query B executes, everything is OK, contains X and Y
Open screen A again - no query, data only contain X and Y

From this I presume query A has to be loaded before query B is loaded for the A query to break.

This is not the first time I run into this problem. Previously we solved it by using fragments to always load all the data, because it was not that big of a overhead. This time it is a bit larger problem, because there is lot more data.

@andrepcg
Copy link

I'm also having trouble with this behavior

@evans
Copy link
Contributor

evans commented Sep 7, 2018

@MrLoh Thank you for reporting the issue and digging into! I'm working on a reproduction: https://codesandbox.io/s/lyj02q5347. It seems like the query works for first order values under the root query, so we'll have to create a little more complicated repro. If you have a chance to fork the codesandbox and associated https://glitch.com/edit/#!/worried-song-1 to create a repro, that would be incredible!

I'm going to be pulled away from the problem for a bit, so may not have a chance to finish up the repro fully in the next couple of weeks

@thesubway
Copy link

I also ran into this issue (random, 20% of the time it still worked) when using the same child to 2 different object types.


#comments is a child here:
order {
  total
  comments {
    text
  }
}

#comments is also a child here:
user {
  id
  comments {
    text
  }
}

This was fixed when I changed the __typename for user comment to be different from the __typename for order comment.

@amiiit
Copy link

amiiit commented Oct 31, 2018

Seems like adding the prop partialRefetch is a reasonable workaround and otherwise we will have to wait patiently to v3 😿

https://www.apollographql.com/docs/react/essentials/queries.html

@hwillson hwillson self-assigned this Nov 27, 2018
@hwillson
Copy link
Member

If anyone here is able to continue the work in #2920 (comment) and provide a runnable reproduction that shows this happening, that would be awesome!

@anysunflower
Copy link

meet the question too

query ProductGraph {

    nodes {    
      products{   
        id   
        name   
      relationships {  
        id  
        start_node  
        end_node  
        type  
      }  
      related_nodes {
        product_push_times {
          name
          uid
          usage
          create_time
          progress_str
          id
        }
    }
      } 
}

}

I can only get id and name in relationships and related_nodes
but id or name of products can not get

@anysunflower
Copy link

meet the question too

query ProductGraph {

    nodes {    
      products{   
        id   
        name   
      relationships {  
        id  
        start_node  
        end_node  
        type  
      }  
      related_nodes {
        product_push_times {
          name
          uid
          usage
          create_time
          progress_str
          id
        }
    }
      } 
}

}

I can only get id and name in relationships and related_nodes
but id or name of products can not get

my current solution is use Aliases of graphql
like this

  nodes{
    products{
      _id: id
      uid
      _name: name
      relationships{
        id
      }
      related_nodes{
        product_push_times{
          id
          name
       }
      }
    }
  }   

that I can get the result _id and _name with the right value from graphql server

and the return json property name would be _id and _name

@hwillson
Copy link
Member

This has been addressed by #4743, and will be coming in Apollo Client 2.6. You can set returnPartialData to true to have previous partial cache results used, instead of having the returned data set to an empty object.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 16, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests