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)

Yeah I overlooked a huge step in connecting Apollo to AppSync. I ended up subclassing RequestBodyCreator and fitting it to the AppSync docs.

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

but to get rid of the strange error I forked Apollo and edited like so

It had nothing to do with Apollo at all except for the start_ack not being implemented :sweat_smile:

I’m having a similar problem in my IOS app.
I’m sorry but i cant really piece together how your finished solution would look like.

Do you have some complete code on how you manage to solve the problem?

Like how you manage the connection and create the subscription?