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.

 note
Starting with 4.0.0, Apollo Kotlin provides a new experimental WebSockets implementation that provides better defaults, better retry and error handling APIs. It aims to replace the current implementation in the future.If you are interested in trying it out, please consult the Experimental WebSockets page.

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:

Kotlin
1apolloClient.subscription(MySubscription())
2    .toFlow()
3    .onEach {
4      // Throw the exception so we can use retryWhen
5      it.exception?.let { throw it }
6    }
7    .retryWhen { e, attempt ->
8      delay(2.0.pow(attempt.toDouble()).toLong())
9      // retry after the delay
10      true
11    }
12    .collect {
13      println("trips booked: ${it.data?.tripsBooked}")
14    }

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:

Kotlin
1    val apolloClient = ApolloClient.Builder()
2        .httpServerUrl("http://localhost:8080/graphql")
3        .webSocketServerUrl("http://localhost:8080/subscriptions")
4        .webSocketReopenWhen { e, attempt ->
5          delay(2.0.pow(attempt.toDouble()).toLong())
6          // retry after the delay
7          true
8        }

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:

JSON
1{
2  "id": "3f32558f-49a3-4dc2-9d1c-445adf0a66c7",
3  "type": "error",
4  "payload": { }
5}

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:

Kotlin
1apolloClient.subscription(MySubscription())
2    .toFlow()
3    .onEach {
4      // Throw the exception so we can use retryWhen
5      it.exception?.let { throw it }
6    }
7    .retryWhen { e, attempt ->
8      if (e is SubscriptionOperationException) {
9        // handle the operation error
10      } else {
11        // handle global errors that were not caught by webSocketReopenWhen
12      }
13    }
14    .collect {
15      println("trips booked: ${it.data?.tripsBooked}")
16    }
Feedback

Edit on GitHub

Forums