Android Apollo v3 - Subscriptions with authentication

Hey Folks!

I try to subscribe our Android client with the Apollo client v3.0.0-beta05. For me it is not possible so do a successful subscription. This is how i create the Apollo-Client:

val graphQLWsProtocol = GraphQLWsProtocol.Factory(connectionPayload = mapOf("Authorization" to "Bearer ..."))

ApolloClient
                    .Builder()
                    .httpServerUrl(httpServerUrl = httpServerUrl)
                    .httpEngine(DefaultHttpEngine(get<OkHttpClient>()))
                    .subscriptionNetworkTransport(WebSocketNetworkTransport
                                                          .Builder()
                                                          .serverUrl("...")
                                                          .protocol(graphQLWsProtocol)
                                                          .webSocketEngine(DefaultWebSocketEngine(get<OkHttpClient>()))
                                                          .build())
                    .addCustomScalarAdapter(GraphQLISODateTime.type, DateAdapter())
                    .build()

Is this the right way to add an authentication to wss? My error response is:

Caused by: com.apollographql.apollo3.exception.ApolloWebSocketClosedException: WebSocket Closed code='1002' reason=''
        at com.apollographql.apollo3.network.ws.DefaultWebSocketEngine$open$webSocket$1.onClosing(OkHttpWebSocketEngine.kt:60)
        at okhttp3.internal.ws.RealWebSocket.onReadClose(RealWebSocket.kt:378)
        at okhttp3.internal.ws.WebSocketReader.readControlFrame(WebSocketReader.kt:220)
        at okhttp3.internal.ws.WebSocketReader.processNextFrame(WebSocketReader.kt:104)
        at okhttp3.internal.ws.RealWebSocket.loopReader(RealWebSocket.kt:293)
        at okhttp3.internal.ws.RealWebSocket$connect$1.onResponse(RealWebSocket.kt:195)
        at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:920)

Maybe someone of you have an idea.

Thx!

1 Like

Hi :wave:! That does look appropriate…

Is there any chance your API is public? We could then double check. But if it’s not, I don’t think there’s a standard way to handle authentication so it’s hard to tell.

At this point, I’d recommend double checking the url. If that’s not enough, usually running through a proxy like Charles will highlight the difference and hint at what the problem might be.

Hey Martin,

thanks for your answer! The API is public but need an authentication. I can send you via PM the URL?!

1 Like

I can send you via PM the URL?!

Yep, feel free to send information at martin@apollographql.com

Also, something that just came to mind. Are you sure you need GraphQLWsProtocol? There are 2 protocols:

  • SubscriptionWsProtocol is the default protocol supported by apollo-server
  • GraphQLWsProtocol is a new protocol that can also be used with apollo-server and also others frameworks like express and others.

I’m no expert in the server space but I’d double check which one you need.

Haha, nope. Because we can’t find some documentation, we try some things. I send you the URL via email.

Oh, that looks good! If we use the SubscriptionWsProtocol class. We can subscribe and we get a push into the app! Thanks a lot @mbonnin

val graphQLWsProtocol = SubscriptionWsProtocol.Factory(connectionPayload = {
    mapOf("Authorization" to "Bearer ...")
})
1 Like

Nice :tada:Thanks for the update :blush:

Ok, now i got the subscription and everything works. The SubscriptionWsProtocol is part of the ApolloClient instance and this is a singleton.

But, is there an option to refresh the Token in the SubscriptionWsProtocol? (The Interceptors work only on http and not with ws calls)
What happen if the token is invalid?

Thx & greetings,
Daniel

What happen if the token is invalid?

This is a very good question and the fact that most implementations are server-dependent make it very hard to test against. Not too mention the relative absence of public GraphQL servers supporting subscriptions.

In your specific case, I’d expect that the WebSocket would throw an error and that you would be able to restart it? SubscriptionWsProtocol.connectionPayload is a lambda so you can execute code to refresh your token inside the lambda if needed.

You might need to use the beta06-SNAPSHOT for this as we recently fixed a bug when errors happen.

In all cases, if you ever have a network dump of what happens when the token expires, it’d be awesome if you could share it, we could add that to unit tests.

1 Like

I just migrated my Apollo client from version 2 to version 3 (3.8 specifically) and my subscriptions doesn’t work.

Heres the version 2 config:

     .subscriptionConnectionParams(
                SubscriptionConnectionParams().apply { put("Authorization", accessToken) }
            )
            .subscriptionTransportFactory(
                WebSocketSubscriptionTransport.Factory(
                    wsServerUrl,
                    okHttpClient
                )
            )

Here’s the version 3 config:

.webSocketServerUrl(wsServerUrl)
.wsProtocol(SubscriptionWsProtocol.Factory(connectionPayload = { mapOf("Authorization" to accessToken) }))

This seems correct. Do you have any more information what goes wrong? Any exception or anything?

To debug these kind of things, I usually run both apps through a web proxy like Charles that should show the difference.

Here’s what I’m doing:

subscription(MySubscription())
    .toFlow()
    .catch { print(it.message) }

I’m getting a SubscriptionOperationException with cause = null and message = “Operation error on MySubscription” | payloadMessage = message = “Must provide document.”.

Do I need to care about some incompatibility with the server version? I saw that in the client version 2 the connection payload was inserted to the json message and now we have the following code inside the SubscriptionWsProtocol.kt :

    val message = mutableMapOf<String, Any?>(
        "type" to "connection_init",
    )

    val payload = connectionPayload()
    if (payload != null) {
      message.put("payload", payload)
    }

I’m still having a migration issue with subscriptions. Here’s my apollo client ws config.

val subscriptionWsProtocol = SubscriptionWsProtocol.Factory(
    connectionPayload = { mapOf("Authorization" to accessToken, "x-timezone" to "America/Los_Angeles") }
)
val webSocket = WebSocketNetworkTransport.Builder()
    .serverUrl(GRAPHQL_SUBSCRIPTION_WEBSOCKET_URL)
    .protocol(subscriptionWsProtocol)
    .addHeader("Accept-Encoding", "gzip, deflate, br")
    .addHeader("Accept-Language", "en,zh-CN;q=0.9,zh;q=0.8")

The connection is working fine, but I’m having an error when I’m trying to subscribe with the following error message: Must provide document.

I’m seeing that document in this case means Operation.DOCUMENT, and this field is passed to the query field in the payload by calling Operation.document() inside the DefaultHttpRequestComposer.composePayload(request).

if (sendDocument) {
    queryParams.put("query", operation.document())
}

But, debugging the connection payload I realized that the query field is null, even that the sendDocument flag is enabled by default for the SubscriptionWsProtocol.

What should I do to fix this problem? Because on the iOS side we have the query filled with the Operation.DOCUMENT properly.

Did you enable Auto Persisted Queries by any chance? It might set sendDocument to false.

If you have a debugger hooked, look at the value of sendDocument, if you can confirm sendDocument = false, you can force it to true with:

   apolloClient.subscription(subscription)
          .sendDocument(true)
          .toFlow()

Yes, the persisted queries are being enabled in the Apollo Client build phase.

I did some tests about the Persisted Queries and sendDocument(true), if we use persisted query the document is not sent, even if we force it using sendDocument(true). So, I just created an extension for the subscription method that disable persisted queries for subscriptions

fun <D : Subscription.Data> ApolloClient.notPersistedSubscription(subscription: Subscription<D>): ApolloCall<D> {
    return subscription(subscription).enableAutoPersistedQueries(false)
}

Would be helpful if we have the possibility to disable persisted queries for subscriptions by default using the ApolloClient.Builder.

Thanks for the help, its working after the changes.

1 Like