Using Apollo with AppSync directly on iOS

For the past few days I have been trying to get the apollo-ios subscriptions to work with AppSync with no luck. I am able to fetch the schema, generate the API.swift file, and run querys but when I try to setup the WebSocketTransport no matter what I pass I am getting

{
    errorType = UnsupportedOperation;
    message = "unknown not supported through the realtime channel";
}

I am setting up the Apollo Client like this:

class Network {
    static let shared = Network()
    private lazy var cache = InMemoryNormalizedCache()
    private lazy var store = ApolloStore(cache: cache)

    private lazy var webSocketTransport: WebSocketTransport = {

        let authDict = [
            "x-api-key": "API_KEY",
            "host": "<API_URL>.appsync-api.ap-northeast-1.amazonaws.com"
        ]

        let headerData: Data = try! JSONSerialization.data(withJSONObject: authDict, options: JSONSerialization.WritingOptions.prettyPrinted)
        let headerBase64 = headerData.base64EncodedString()

        let payloadData = try! JSONSerialization.data(withJSONObject: [:], options: JSONSerialization.WritingOptions.prettyPrinted)
        let payloadBase64 = payloadData.base64EncodedString()

        let authPayload: [String : JSONEncodable] = [
            "header": headerBase64,
            "payload": payloadBase64
        ]

        let url = URL(string: "wss://<API_URL>.appsync-realtime-api.ap-northeast-1.amazonaws.com/graphql?header=\(headerBase64)&payload=\(payloadBase64)")!
        let request = URLRequest(url: url)

        return WebSocketTransport(request: request, sendOperationIdentifiers: false, reconnectionInterval: 5)
    }()

    /// An HTTP transport to use for queries and mutations
    private lazy var normalTransport: RequestChainNetworkTransport = {
        let url = URL(string: "https://<API_URL>.appsync-api.ap-northeast-1.amazonaws.com/graphql")!
        return RequestChainNetworkTransport(interceptorProvider: LegacyInterceptorProvider(store: store), endpointURL: url, additionalHeaders: ["X-Api-Key": "API_KEY"])
    }()

    /// A split network transport to allow the use of both of the above
    /// transports through a single `NetworkTransport` instance.
    private lazy var splitNetworkTransport = SplitNetworkTransport(
        uploadingNetworkTransport: self.normalTransport,
        webSocketNetworkTransport: self.webSocketTransport
    )

    private(set) lazy var apollo: ApolloClient = {
        return ApolloClient(networkTransport: splitNetworkTransport, store: store)
    }()

and when setting up a subscription I did:

class ViewController: UIViewController {
    var subscription: Cancellable?

    override func viewDidLoad() {
        super.viewDidLoad()
        subscription = Network.shared.apollo.subscribe(subscription: SubscribeCommentsSubscription()) { result in
            switch result {
            case .success(let graphQLResult):
              print("Success! Result: \(graphQLResult)")
            case .failure(let error):
              print("Failure! Error: \(error)")
            }
        }
    }

    deinit {
      // Make sure the subscription is cancelled, if it exists, when this object is deallocated.
      self.subscription?.cancel()
    }
}

Just to share a snippet from API.swift

    """
    subscription subscribeComments {
      subscribeToEventComments(eventId: "EVENT_ID") {
        __typename
        eventId
        commentId
        content
      }
    }
    """

A similar setup is working on Android so I am at a loss for what is not working correctly…

In Addition to the above I tried creating my own body for the Subscription Request to no avail…

class AppSyncRequestBodyCreator: RequestBodyCreator {
    func requestBody<Operation>(for operation: Operation, sendOperationIdentifiers: Bool, sendQueryDocument: Bool, autoPersistQuery: Bool) -> GraphQLMap where Operation : GraphQLOperation {
        var body: GraphQLMap = [:
//          "variables": operation.variables
//          "operationName": operation.operationName,
        ]

        if sendOperationIdentifiers {
          guard let operationIdentifier = operation.operationIdentifier else {
            preconditionFailure("To send operation identifiers, Apollo types must be generated with operationIdentifiers")
          }

            body["id"] = UUID().uuidString
        }

        if sendQueryDocument {
            let data = try! JSONSerialization.data(withJSONObject: ["query": operation.queryDocument,
                                                                    "variables": [:]], options: .prettyPrinted)
            let jsonString = String(data: data, encoding: .utf8)
            body["data"] = jsonString
        }

        let authDict = [
            "x-api-key": "API_KEY",
            "host": "API_URL.appsync-api.ap-northeast-1.amazonaws.com",
        ]
        body["extension"] = ["authorization": authDict]

        if autoPersistQuery {
          guard let operationIdentifier = operation.operationIdentifier else {
            preconditionFailure("To enable `autoPersistQueries`, Apollo types must be generated with operationIdentifiers")
          }

          body["extensions"] = [
            "persistedQuery" : ["sha256Hash": operationIdentifier, "version": 1]
          ]
        }

        return body
    }
}

I do not have a solution but am experiencing the same problem on Mac with Node.js (v16) - my code is described in Using `client.subscribe` does not work? (to AppSync, from Node) When I add logs to the ws implementation, I see:

ws.msg {"type":"connection_ack","payload":{"connectionTimeoutMs":300000}}
ws.msg {"type":"ka"}
ws.msg {"type":"error","payload":{"errors":[{"errorType":"UnsupportedOperation","message":"unknown not supported through the realtime channel"}]}}

I suppose I got to the “connection init ack” as described in the docs. The error then I guess comes from AppSync. However the Python client - also referenced from my post - works just fine. There the messages are:

### opened ###
>> {"type": "connection_init"}
### message ###
<< {"type":"connection_ack","payload":{"connectionTimeoutMs":300000}}
>> {"id": "04acda54-438d-4558-8472-211b76df0f73", "payload": {"data": "{\"query\": \"subscription JhTestSubscription {onOrderAndRequestOwnerConsent(id: \\\"281c6d08-146b-4b05-aeb9-ef12c5ed1cc5\\\") {id,orderId,state}}\", \"variables\": {}}", "extensions": {"authorization": {"host": "<secret>.appsync-api.eu-west-1.amazonaws.com", "x-api-key": "da2-<secret>"}}}, "type": "start"}

The changes described here Document how to use client with AppSync · Issue #224 · apollographql/apollo-feature-requests · GitHub solved the problem for me. You would need to do something similar in the iOS code, I imagine. No idea why a similar setup works on Android.

(Edited by admin @AnthonyMDev to fix broken link)