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

React Native Uncaught Error, Network Request Failed #10559

Closed
Stevemoretz opened this issue Feb 15, 2023 · 21 comments · Fixed by #11162
Closed

React Native Uncaught Error, Network Request Failed #10559

Stevemoretz opened this issue Feb 15, 2023 · 21 comments · Fixed by #11162

Comments

@Stevemoretz
Copy link

Stevemoretz commented Feb 15, 2023

Issue Description

When using the subscribe function

try{
                        await ApolloClient.subscribe<GQLSubscription>({
                            query: gql(/* GraphQL */ `
                                subscription {
                                    postUpdated {
                                        ID
                                        rooms
                                    }
                                }
                            `),
                            errorPolicy: "all",
                            context: {
                                uri: "http://graphql.test/wp-json/api/graphql",
                            },
                        }).subscribe((value) => {
                            console.log("subscribe", value);
                        });
}catch(e){

}

If the network is gone or any error happens it is thrown outside my catch! Even though errorPolicy: "all", shouldn't throw at all, it happens even if I disable all the links:

        globalThis.ApolloClient = new OApolloClient({
            cache: new InMemoryCache({}),
            uri: "http://dummy.test",
        });

Screen Shot 1401-11-26 at 13 26 19

It's works fine for query, and mutation just the subscribe throws that weird uncatchable error!

Link to Reproduction

https://github.com/Stevemoretz/apollo-bug

Reproduction Steps

git clone https://github.com/Stevemoretz/apollo-bug
cd apollo-bug
yarn install
npx expo start

Press i for iOS and a for Android, after it opens up you'll see a button:

Screen Shot 1401-11-26 at 15 11 55

Press it and you'll get :

Screen Shot 1401-11-26 at 15 12 14

Here's the whole code:
https://github.com/Stevemoretz/apollo-bug/blob/main/App.tsx

@jerelmiller
Copy link
Member

Hey @Stevemoretz 👋 Thanks for opening the issue!

You're right, it looks like subscribe doesn't adhere to the errorPolicy and will always throw when there is an error. Let me ask around to see if I can discover why this might be the case.


As for throwing the error outside of your try/catch, something to keep in mind is that the value returned from ApolloClient.subscribe is an Observable (specifically zen-observable's implementation), which is an async data type. try/catch is a synchronous language construct, so if an error is thrown asynchronously, unfortunately your try/catch isn't going to catch anything.

You can replicate this behavior using setTimeout:

try {
  setTimeout(() => {
    throw new Error()
  }, 100);
} catch (e) {
  console.log('caught error!', e)
}

If you'd like to handle errors from the observable, try passing an error handler to your subscribe function.

ApolloClient
  .subscribe({...})
  .subscribe(
    (value) => console.log('subscribe', value),
    (error) => console.error('error', error)
  })

// or if you prefer the object-based API
ApolloClient
  .subscribe({...})
  .subscribe({
    next: (value) => console.log('subscribe', value),
    error: (error) => console.error('error', error)
  })

@Stevemoretz
Copy link
Author

Hey @Stevemoretz 👋 Thanks for opening the issue!

You're right, it looks like subscribe doesn't adhere to the errorPolicy and will always throw when there is an error. Let me ask around to see if I can discover why this might be the case.

As for throwing the error outside of your try/catch, something to keep in mind is that the value returned from ApolloClient.subscribe is an Observable (specifically zen-observable's implementation), which is an async data type. try/catch is a synchronous language construct, so if an error is thrown asynchronously, unfortunately your try/catch isn't going to catch anything.

You can replicate this behavior using setTimeout:

try {
  setTimeout(() => {
    throw new Error()
  }, 100);
} catch (e) {
  console.log('caught error!', e)
}

If you'd like to handle errors from the observable, try passing an error handler to your subscribe function.

ApolloClient
  .subscribe({...})
  .subscribe(
    (value) => console.log('subscribe', value),
    (error) => console.error('error', error)
  })

// or if you prefer the object-based API
ApolloClient
  .subscribe({...})
  .subscribe({
    next: (value) => console.log('subscribe', value),
    error: (error) => console.error('error', error)
  })

Thank you indeed that's very helpful thanks for explaining! I haven't had noticed those callbacks now I noticed there is also another callback called 'complete' this one gets called if there weren't any errors and it successfully connected?

@jerelmiller
Copy link
Member

@Stevemoretz not quite. The complete is meant to represent the point at which the observable has finished execution. Think of it like Promise.resolve(), but for observables. Once an observable completes, its done and can no longer emit values.

In the case of Apollo subscriptions, assuming you're using GraphQLWsLink and graphql-ws, I think complete isn't called until the subscription is closed, which I believe also happens when the websocket terminates.

I'm willing to bet you'll find less value in complete in this case, unless you want to do something when the subscription is complete.

@Stevemoretz
Copy link
Author

@Stevemoretz not quite. The complete is meant to represent the point at which the observable has finished execution. Think of it like Promise.resolve(), but for observables. Once an observable completes, its done and can no longer emit values.

In the case of Apollo subscriptions, assuming you're using GraphQLWsLink and graphql-ws, I think complete isn't called until the subscription is closed, which I believe also happens when the websocket terminates.

I'm willing to bet you'll find less value in complete in this case, unless you want to do something when the subscription is complete.

Thanks Jerel, I was wondering what that does, I tested start too that also doesn't get called at all, so right now there is no way to find out wether the request was successful; we can get the error though if it isn't. Maybe errorPolicy shouldn't be supported at all (based on the output it gives I think that would be hard to implement for you) but a callback for the connection was successful (onSubscribed) here would be as helpful without needing to change too much in the library's code.

@jerelmiller jerelmiller removed the 🏓 awaiting-team-response requires input from the apollo team label Feb 21, 2023
@jerelmiller
Copy link
Member

Hey @Stevemoretz 👋

I had a chance to talk with the team a bit more in-depth about this and we agree that subscriptions should respect the errorPolicy set in options. Conceptually, we shouldn't be treating subscriptions much different than regular queries, with the difference being a push vs pull model where the server can push payloads to the client. Thinking in this vein, Apollo Client doesn't quite live up to that standard, so we absolutely should make sure to respect the error policy here and get that fixed.


In terms of start, could you point me to where you're seeing that? I don't seem to be understanding where start is coming from.

If you need to check for the socket connection, you should be able to get socket events via graphql-ws's on function on the socket client. You can see a list of events here.

import { createClient } from 'graphql-ws';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';

const socketClient = createClient({ uri: '...' });

socketClient.on('connected', (event) => {
  console.log('websocket connected');
});

const socketLink = new GraphQLWSLink(socketClient);

If this isn't sufficient for your needs, I'd love to get a better understanding of how an onSubscribed callback would benefit here.

@Stevemoretz
Copy link
Author

Hey @Stevemoretz 👋

I had a chance to talk with the team a bit more in-depth about this and we agree that subscriptions should respect the errorPolicy set in options. Conceptually, we shouldn't be treating subscriptions much different than regular queries, with the difference being a push vs pull model where the server can push payloads to the client. Thinking in this vein, Apollo Client doesn't quite live up to that standard, so we absolutely should make sure to respect the error policy here and get that fixed.

In terms of start, could you point me to where you're seeing that? I don't seem to be understanding where start is coming from.

If you need to check for the socket connection, you should be able to get socket events via graphql-ws's on function on the socket client. You can see a list of events here.

import { createClient } from 'graphql-ws';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';

const socketClient = createClient({ uri: '...' });

socketClient.on('connected', (event) => {
  console.log('websocket connected');
});

const socketLink = new GraphQLWSLink(socketClient);

If this isn't sufficient for your needs, I'd love to get a better understanding of how an onSubscribed callback would benefit here.

Thank you for going through with this, start is just another property you can pass to that just like error, but I have no idea what it does it was in typescript types without any comments.

What I was really interested to know was whether the response from server was successful or not but currently there is no callback for that, so what I did instead was to wrap this whole thing in my own function which returns a promise, and if for 30 seconds it doesn't throw any error it resolves otherwise it throws the error.

30 second is the timeout for axios which is being used in apollo, so bascially my custom function takes 30 seconds to resolve which isn't ideal but it works, if I could get that response I could resolve it right after it but I can't, other than that I am fine with the way it is right now.

@jerelmiller
Copy link
Member

jerelmiller commented Feb 21, 2023

start is just another property you can pass to that just like error

When you say "that", are you referring to the client created from graphql-ws or the observable created from calling .subscribe(...)? Would you be willing to link me somewhere where you're seeing start (either code or docs is fine). Apologies for not quite understanding, I must not have had enough coffee this morning 😅 .

I was really interested to know was whether the response from server was successful or not

I'd love to explore this a bit more. I'd like understand why this is necessary. To be more specific, is there a particular UX pattern or UI element you're trying to build that would benefit from knowing when the connection itself was successful?

The next function on the observable should get called as soon as data is pushed from your server to the client and that is a pretty good signal that everything was connected properly. Is this suitable enough for your needs? Perhaps I'm misunderstanding what you mean by "response from server" in this case and I'd if you could give me some more detail here.

Thanks for hanging with me!

@Stevemoretz
Copy link
Author

start is just another property you can pass to that just like error

When you say "that", are you referring to the client created from graphql-ws or the observable created from calling .subscribe(...)? Would you be willing to link me somewhere where you're seeing start (either code or docs is fine). Apologies for not quite understanding, I must not have had enough coffee this morning 😅 .

I was really interested to know was whether the response from server was successful or not

I'd love to explore this a bit more. I'd like understand why this is necessary. To be more specific, is there a particular UX pattern or UI element you're trying to build that would benefit from knowing when the connection itself was successful?

The next function on the observable should get called as soon as data is pushed from your server to the client and that is a pretty good signal that everything was connected properly. Is this suitable enough for your needs? Perhaps I'm misunderstanding what you mean by "response from server" in this case and I'd if you could give me some more detail here.

Thanks for hanging with me!

That's the property of the object that you can pass to subscribe method, I don't even use graphql-ws, sorry I'm on my phone a little hard to add code lol

subscribe({
    start:()=>{} 
})

What I meant from the response wasn't the websocket responses but the simple Http request that happens with subscription query in it to the graphql server, that http request surely has a response it could be empty or not I don't care about its content I do care about knowing that the request itself was successful that has nothing to do with websocket.

When you are disconnected from the internet and you call the subscribe function the error callback gets called, but when you are connected and everything goes well nothing gets called to let you know the initial request was successful, that's why I have to wait for 30 seconds to make sure that initial request was successful.

@jerelmiller
Copy link
Member

Ahhhhhh I finally found where you're seeing that: https://github.com/apollographql/zen-observable-ts/blob/main/module.d.ts#L14

That zen-observable-ts package is one that we use internally as a thin wrapper around zen-observable. From what I'm reading, it looks like that start function is actually incorrect. I don't see that available anywhere in the actual zen-observable package. I'll file an issue over there to see if that function can be removed from the interface. This would explain why its not being called in your case 😆 .


At this point, we might be getting a little bit far from the original issue which is that the errorPolicy is ignored for subscriptions. Apologies on my part, this was interesting conversation!

If adding the ability to detect when the subscription is connected is something you're passionate about, could you add a feature request over in our feature requests repo? This would be a great place to continue this conversation and nail down the use cases for this potential feature add.

As for this bug, we will get to it when we get a chance! Thanks for bringing this to our attention.

@Stevemoretz
Copy link
Author

Ahhhhhh I finally found where you're seeing that: https://github.com/apollographql/zen-observable-ts/blob/main/module.d.ts#L14

That zen-observable-ts package is one that we use internally as a thin wrapper around zen-observable. From what I'm reading, it looks like that start function is actually incorrect. I don't see that available anywhere in the actual zen-observable package. I'll file an issue over there to see if that function can be removed from the interface. This would explain why its not being called in your case 😆 .

At this point, we might be getting a little bit far from the original issue which is that the errorPolicy is ignored for subscriptions. Apologies on my part, this was interesting conversation!

If adding the ability to detect when the subscription is connected is something you're passionate about, could you add a feature request over in our feature requests repo? This would be a great place to continue this conversation and nail down the use cases for this potential feature add.

As for this bug, we will get to it when we get a chance! Thanks for bringing this to our attention.

Thank you for explaining that and submitting that issue over there, actually errorPolicy is related to this exact thing, so we don't need another feature request if the errorPolicy would be implemented, on all the other requests (query and mutation) if that request passes then we can access the data and if it fails it returns the error or throws an error object so in those cases we are covered and if errorPolicy gets implemented for this too, I'd suppose the behavior should be the same, so we should be able to even get the data as well as knowing it was a successful request, unless it would be implemented differently which would make my statement completely wrong.

@jerelmiller
Copy link
Member

Ok great!

As for the behavior, it should be identical to how queries work. Refer to the error policy documentation for an overview of the differences and why you might use each.

In practice, I would see it working something similar to this:

client
  .subscribe({ errorPolicy: 'none' }) // the default
  .subscribe({
    next: () => // not called
    error: () => // called since errors are encountered
  })

client
  .subscribe({ errorPolicy: 'ignore' })
  .subscribe({
    next: ({ data, errors }) => // called when an error is encountered. `errors` property is not set
    error: () => // not called
  })

client
  .subscribe({ errorPolicy: 'all' })
  .subscribe({
    next: ({ data, errors }) => // called when an error is encountered. Both `data` and `errors` are available
    error: () => // not called
  })

@Stevemoretz
Copy link
Author

Ok great!

As for the behavior, it should be identical to how queries work. Refer to the error policy documentation for an overview of the differences and why you might use each.

In practice, I would see it working something similar to this:

client
  .subscribe({ errorPolicy: 'none' }) // the default
  .subscribe({
    next: () => // not called
    error: () => // called since errors are encountered
  })

client
  .subscribe({ errorPolicy: 'ignore' })
  .subscribe({
    next: ({ data, errors }) => // called when an error is encountered. `errors` property is not set
    error: () => // not called
  })

client
  .subscribe({ errorPolicy: 'all' })
  .subscribe({
    next: ({ data, errors }) => // called when an error is encountered. Both `data` and `errors` are available
    error: () => // not called
  })

Oh that's where the data and errors would go? Thanks for clarifying so based on what we talked about before next is called when websocket client receives data from the server, but when would errors be filled exactly? I didn't know websocket could actually send errors?

@jerelmiller
Copy link
Member

GraphQL subscription are more-or-less the same as a query, they just typically happen over a different transport (websockets) than a query does (http). errors through the websocket should work just like any other GraphQL query with errors would since subscriptions follow the same GraphQL execution strategy.

@Stevemoretz
Copy link
Author

Stevemoretz commented Feb 21, 2023

GraphQL subscription are more-or-less the same as a query, they just typically happen over a different transport (websockets) than a query does (http). errors through the websocket should work just like any other GraphQL query with errors would since subscriptions follow the same GraphQL execution strategy.

That is quite weird, maybe because I'm using a set of different tools (Lighthouse php + Laravel echo + pusher js), but from what I got the way it works here is

client -----regular http request for subscription to a topic-----> server --------regular http response------> back to client ------> if it was successful client starts listening on that topic (on websocket)

And that's the only query that happens, from now on if anything is sent to that topic the client will reach it inside the websocket connection and that's how we get that data on next function.

I was pretty sure this is how it works and that error and data should be on that first request (also the first subscribe function in your example not the second one) not the websocket part; either ways, one of us is wrong and I think we better double check this part.

@jerelmiller
Copy link
Member

I suppose there is probably a lot of nuance to my original statement because it depends a LOT on the backend you're connected to and how it handles requests 😄. Totally possible the way in which you're using subscriptions goes through the flow you mentioned above, so take my statement with a grain of salt. The way in which I described is the typical flow (at least in the apps I've worked with), but not necessarily the only way in which GraphQL subscriptions are established.

Regardless of the transport or how it the query gets from A -> B, the GraphQL execution itself should be the same across servers implementing against the GraphQL spec. In that regard, the data and errors properties are what Apollo Client really cares about since these are properties of the GraphQL spec for any given response. In this way, it theoretically shouldn't matter how the connection is established, just that Apollo is able to interpret the payload in the correct way and do something with it.

@Stevemoretz
Copy link
Author

I suppose there is probably a lot of nuance to my original statement because it depends a LOT on the backend you're connected to and how it handles requests 😄. Totally possible the way in which you're using subscriptions goes through the flow you mentioned above, so take my statement with a grain of salt. The way in which I described is the typical flow (at least in the apps I've worked with), but not necessarily the only way in which GraphQL subscriptions are established.

Regardless of the transport or how it the query gets from A -> B, the GraphQL execution itself should be the same across servers implementing against the GraphQL spec. In that regard, the data and errors properties are what Apollo Client really cares about since these are properties of the GraphQL spec for any given response. In this way, it theoretically shouldn't matter how the connection is established, just that Apollo is able to interpret the payload in the correct way and do something with it.

Thanks for explaining been thinking about it a bit more, the subscription query doesn't even change it is one static query per subscription so even if that goes through the websocket transport in a backend system it should get the error once only right? I mean unless it's not efficient and sends that same subscription query more than once to get multiple errors which I don't think is the case at least hopefully.

If it only happens once, what is the point of the first subscribe then? Isn't that where that first request goes to? (for both websocket or Http transports) if the main subscription request would go through the first subscribe function which I thought it did (at least in my case it does) then still the errors and data must be on the first subscribe not the second.

client
  .subscribe()
  .subscribe({

@jerelmiller
Copy link
Member

jerelmiller commented Feb 22, 2023

You're right, the double subscribe is a tad confusing 😅 .

These actually do completely separate things, the functions just happen to be named the same. The first subscribe sets up the GraphQL subscription and returns an Observable. That Observable is what gives you the ability to receive updates from the GraphQL subscription over time. The Observable has a subscribe function that allows you to register handlers to receive updates.

So essentially:

client
  .subscribe() // sets up the the GraphQL subscription and returns an Observable
  .subscribe({...}) // allows you to subscribe to updates from the GraphQL subscription

I'm not sure how much you've worked with observables prior to this, but regardless, I'd highly recommend checking out this blog post which does a great job of explaining how they work.

Hopefully that helps!

@marfrede
Copy link

marfrede commented Aug 21, 2023

@jerelmiller Are there any updates on this Issue??

I created another Issue (Issue: For subscriptions, the errorPolicy setting does nothing) for this at https://github.com/kamilkisiela/apollo-angular/ before I knew that the problem actually originated here (@apollo/client).

@jerelmiller
Copy link
Member

Hey @marfrede 👋

No updates yet, but now that we have 3.8 out the door, we can look at pulling this in to our next set of bug fixes. Thanks for the nudge!

@jerelmiller
Copy link
Member

Hey all 👋

This is fixed and has been released in v3.8.2. Thanks again for reporting the bug!

@github-actions
Copy link
Contributor

github-actions bot commented Oct 6, 2023

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
For general questions, we recommend using StackOverflow or our discord server.

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

Successfully merging a pull request may close this issue.

4 participants