Closed Stevemoretz closed 1 year ago
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)
})
Hey @Stevemoretz π Thanks for opening the issue!
You're right, it looks like
subscribe
doesn't adhere to theerrorPolicy
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 fromApolloClient.subscribe
is anObservable
(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 yourtry
/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?
@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 not quite. The
complete
is meant to represent the point at which the observable has finished execution. Think of it likePromise.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
andgraphql-ws
, I thinkcomplete
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.
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.
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 wherestart
is coming from.If you need to check for the socket connection, you should be able to get socket events via
graphql-ws
'son
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.
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!
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 seeingstart
(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.
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.
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 aroundzen-observable
. From what I'm reading, it looks like thatstart
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.
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
})
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?
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.
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.
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.
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
anderrors
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({
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!
@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).
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!
Hey all π
This is fixed and has been released in v3.8.2. Thanks again for reporting the bug!
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.
Issue Description
When using the subscribe function
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: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
Press
i
for iOS anda
for Android, after it opens up you'll see a button:Press it and you'll get :
Here's the whole code: https://github.com/Stevemoretz/apollo-bug/blob/main/App.tsx