Subscription with AppSync needs update requestBodyCreator after token expiration

I’m using subscriptions with AppSync, so i need to override the requestBodyCreator parameters to make it work with AppSync.

AppSync’s server protocol is graphql-ws and the payload using Amazon Cognito user pools is the following:

{
    "id": "subscriptionId",
    "type": "start",
    "payload":
    {
        "data":
        {
            "query": "query string",
            "variables": "variables"
        },
        "extensions":
        {
            "authorization":
            {
                "host": "host",
                "Authorization": "accessToken"
            }
        }
    }
}

This is not a problem on the first launch when i create the webSocketTransport passing my custom AppSyncRequestBodyCreator with a valid access token but when I put my app in the background and bring it to the foreground after an hour (the token has expired), the WebSocket disconnects (didDisconnectWithError and reconnects (webSocketTransportDidReconnect ) , but I no longer receive updates from the subscriptions.

By logging webSocketTransport.subscriptions , the subscriptions still appear to be active, but with an expired token.

Subscriptions ["1": "{\"id\":\"1\",\"payload\":{\"data\":\"{\\n  \\\"query\\\" : \\\"subscription *** } }\\\"\\n}\",\"extensions\":{\"authorization\":{\"Authorization\":\"**** expired token",\"host\":\"***"}},\"operationName\":\"onCalendarChangedSubscription\"},\"type\":\"start\"}", "2": "{\"id\":\"2\",\"payload\":{\"data\":\"{\\n  \\\"query\\\" : \\\"subscription OnSensorSyncSubscription { *** } }\\\"\\n}\",\"extensions\":{\"authorization\":{\"Authorization\":\"*** expired token",\"host\":\"**\"}},\"operationName\":\"OnSensorSyncSubscription\"},\"type\":\"start\"}"]

I think the problem is that on reconnection, the subscription start fails because the token has expired.

When the WebSocket disconnects, I call the updateHeaderValues and updateConnectingPayload methods, passing the updated token. (I doubt that the second method, updateConnectingPayload, is necessary, as the situation does not change.)

extension ApolloClientService: WebSocketTransportDelegate {
    func webSocketTransportDidConnect(_ webSocketTransport: WebSocketTransport) {
        logger.info("WebSocket connected", subsystem: Self.tag)
    }

    func webSocketTransportDidReconnect(_ webSocketTransport: WebSocketTransport) {
        logger.info("WebSocket reconnected", subsystem: Self.tag)
        printSubscriptions(webSocketTransport)
    }

    func webSocketTransport(_ webSocketTransport: WebSocketTransport, didDisconnectWithError error: (any Error)?) {
        Task {
            let accessToken = try await authTokenDataSource.fetchAccessToken()
            webSocketTransport.updateHeaderValues(["Authorization": accessToken], reconnectIfConnected: true)
            webSocketTransport.updateConnectingPayload(buildAuthenticationPayload(), reconnectIfConnected: true)
            printSubscriptions(webSocketTransport)
        }
    }

    private func printSubscriptions(_ webSocketTransport: WebSocketTransport) {
        if let subscriptions = webSocketTransport.getProperty(name: "subscriptions") as? [String: String] {
            logger.trace("Subscriptions \(subscriptions.count)", subsystem: Self.tag)
            logger.trace("Subscriptions \(subscriptions)", subsystem: Self.tag)
        }
    }
}

I need a way to update the Request body creator upon reconnection, but I can’t find a way to do it. Or any other strategy to make the subscription work again.

Have you any suggestion?

Hi @andr3a88-amk :wave:

Does your auth token need to be in the request body or the connecting payload, or both?

When the WebSocket disconnects, I call the updateHeaderValues and updateConnectingPayload methods, passing the updated token. (I doubt that the second method, updateConnectingPayload, is necessary, as the situation does not change.)

This leads me to believe that the auth token does not need to be in the connecting payload?

I haven’t visited this code in a while but is the request body creator only called during the very first request creation?

Hi Calvin,

From what I can see, when replicating the WebSocket initial connection with Postman, the auth token is not required in the connection payload but only as a header on the request. This is due to the AppSync protocol.

As you mentioned, the problem is that the request body creator is computed only once when the Apollo client is created. My colleague on Android doesn’t have this issue; I was investigating Apollo-Kotlin, and from this file (line 41), the authentication object is computed multiple times.

Do you think it is somehow possible to align the behavior to that of Android?

So are you using the request headers in your request body creator to build extensions.authorization? That would make sense why changing the headers has no effect on the request body - is that right?

@calvincestari i think the same “refreshed” accessToken should be used to update both the header values (via updateHeaderValues) and the request body creator, but i cannot find a way to update the latter. I tried with updateConnectingPayload but viewing the source code is no the right call.

I don’t think it’s about aligning behaviour with Apollo Kotlin but rather deciding whether it’s correct to call the request body creator on each reconnect. Being able to update the request headers, and that in turn reconnecting, would hint that the request body creator should maybe be called again too.

A possible solution could be to have an updateRequestBodyCreator on the web socket transport (like updateHeaderValues) or to call

requestBody<Operation: GraphQLOperation>(for operation: Operation, sendQueryDocument: Bool, autoPersistQuery: Bool) -> JSONEncodableDictionary

of the custom request creator when a disconnection occurs.

I’m going to look into this today. I think it might be OK to just call the existing request body creator again on reconnect instead of a new function to set a new request body creator when that isn’t what we’re actually wanting.

I spent some time on this today and it doesn’t seem appropriate to recreate the message body on a reconnect. If you look here you will see that the message is stored for the request id, and then on reconnect all stored messages are resent. This makes sense because message bodies shouldn’t change. The fact that AppSync wants auth data in payload.extensions is very specific to their way of handling those messages.

Being able to update the header values and connecting payload is kinda OK because that data is used for the whole websocket connection before any individual messages have been sent.

I think your best option is to close the subscription and resubscribe. It’s not great but this is the workaround you’ll need to use with AppSync unfortunately.