Handle WebSocket errors
Because the WebSocket is a shared resource, error handling is different from regular queries. In HTTP queries, an error has a single consumer. With WebSockets, an error may have multiple consumers.
WebSocket errors with multiple consumers
When your network goes down or your server closes the connection (because it wants a new token or something else), all the subscriptions are terminated. You could retry them individually. The example below uses a very naive exponential backoff algorithm:
apolloClient.subscription(MySubscription()).toFlow().onEach {// Throw the exception so we can use retryWhenit.exception?.let { throw it }}.retryWhen { e, attempt ->delay(2.0.pow(attempt.toDouble()).toLong())// retry after the delaytrue}.collect {println("trips booked: ${it.data?.tripsBooked}")}
This has a number of shortcomings (not mentioning the naive exponential backoff implementation):
- This code has to be implemented in different places
- The attempt is tied to the collector lifecycle, there is no way to reset it if the network is back up
Instead you can manage this in a more central place when configuring your ApolloClient:
val apolloClient = ApolloClient.Builder().httpServerUrl("http://localhost:8080/graphql").webSocketServerUrl("http://localhost:8080/subscriptions").webSocketReopenWhen { e, attempt ->delay(2.0.pow(attempt.toDouble()).toLong())// retry after the delaytrue}
The above code will remember all current subscriptions and resubscribe to the server automatically without having to call retry
in each screen.
WebSocket errors with single consumers
In some cases, you may have a situation where an error is for a single consumer. This happens typically for error messages:
{"id": "3f32558f-49a3-4dc2-9d1c-445adf0a66c7","type": "error","payload": { }}
In these cases, the Flow will emit an ApolloResponse
with a SubscriptionOperationException
in its exception
and then terminate. You can handle the retry like so:
apolloClient.subscription(MySubscription()).toFlow().onEach {// Throw the exception so we can use retryWhenit.exception?.let { throw it }}.retryWhen { e, attempt ->if (e is SubscriptionOperationException) {// handle the operation error} else {// handle global errors that were not caught by webSocketReopenWhen}}.collect {println("trips booked: ${it.data?.tripsBooked}")}