aws appsync subscription not working with flutter graphql_flutter package

2.8k views Asked by At

my pubspec.yaml

dev_dependencies:
  flutter_test:
    sdk: flutter
  graphql_flutter: ^4.0.0

roomctrl.dart

import 'package:flutter/material.dart';
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:innospace/config/appsync-service.dart';
import 'package:innospace/config/client_provider.dart';

class RoomCtrl extends StatefulWidget {
  RoomCtrl({Key key}) : super(key: key);

  _RoomCtrlState createState() => _RoomCtrlState();
}

class _RoomCtrlState extends State<RoomCtrl> {

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return ClientProvider(
        child: Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: <Widget>[
         Subscription(
        options:SubscriptionOptions(
            document: gql(AppSyncService.onUpdateStateTestSubscription)),
          builder: (result) {
            if (result.hasException) {
              return Text(result.exception.toString());
            }

            if (result.isLoading) {
              return Center(
                child: const CircularProgressIndicator(),
              );
            }
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: <Widget>[
                  Container(
                    child: Text(result.data.length.toString()),
                  )
                ],
              ),
            );
          },
        )

          ],
        ),
      ),
    ));
  }
}

client_provider.dart

import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:flutter/material.dart';

import 'constants.dart';

String uuidFromObject(Object object) {
  if (object is Map<String, Object>) {
    final String typeName = object['__typename'] as String;
    final String id = object['id'].toString();
    if (typeName != null && id != null) {
      return <String>[typeName, id].join('/');
    }
  }
  return null;
}

ValueNotifier<GraphQLClient> clientFor() {
   const dynamic headers = {
    "headers": {
      "host": AWS_APP_SYNC_ENDPOINT_AUTHORITY,
      "x-api-key": AWS_APP_SYNC_KEY
    }};
   const  sClient= SocketClientConfig(
      autoReconnect : true,
      initialPayload: headers
  );

   final WebSocketLink _webSocketLink =new WebSocketLink(AWS_APP_SYNC_ENDPOINT_WSS,  config:sClient );

   final Link link = _webSocketLink;
  return ValueNotifier<GraphQLClient>(
    GraphQLClient(
      cache: GraphQLCache(),
      link: link,
    ),
  );
}

/// Wraps the root application with the `graphql_flutter` client.
/// We use the cache for all state management.
class ClientProvider extends StatelessWidget {
  ClientProvider({
    @required this.child,
  }) : client = clientFor();

  final Widget child;
  final ValueNotifier<GraphQLClient> client;

  @override
  Widget build(BuildContext context) {
    return GraphQLProvider(
      client: client,
      child: child,
    );
  }
}

constants.dart

[https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html][1]

const AWS_APP_SYNC_ENDPOINT_AUTHORITY = "xxxxxxxxxxxxxxxxxxxxx.appsync-api.ap-southeast-2.amazonaws.com"; 
const AWS_APP_SYNC_ENDPOINT_WSS = "wss://xxxxxxxxxxxxxxxxxxxxx.appsync-realtime-api.ap-southeast-2.amazonaws.com/graphql?header=base64encryption(xxx-xxxxxxxxxxxxxxxxxx as per aws)&payload=e30="; 
const AWS_APP_SYNC_KEY = "xxx-xxxxxxxxxxxxxxxxxx";

subscription query

static String onUpdateStateTestSubscription = '''
        subscription OnUpdateStateTest {
        onUpdateStateTest {
          __typename
          RoomId
          RoomName
        }
      }''';

console

Connecting to websocket: wss://xxxxxxxxxxxxxxxxxxxxx.appsync-realtime-api.ap-southeast-2.amazonaws.com/graphql?header=base64encryption(xxx-xxxxxxxxxxxxxxxxxx as per aws)&payload=e30=...
I/flutter ( 9942): Connected to websocket.
I/flutter ( 9942): Haven't received keep alive message for 30 seconds. Disconnecting..
I/flutter ( 9942): Disconnected from websocket.
I/flutter ( 9942): Scheduling to connect in 5 seconds...
I/flutter ( 9942): Connecting to websocket: wss://xxxxxxxxxxxxxxxxxxxxx.appsync-realtime-api.ap-southeast-2.amazonaws.com/graphql?header=base64encryption(xxx-xxxxxxxxxxxxxxxxxx as per aws)&payload=e30=...
I/flutter ( 9942): Connected to websocket.
I/flutter ( 9942): Haven't received keep alive message for 30 seconds. Disconnecting..
I/flutter ( 9942): Disconnected from websocket.
I/flutter ( 9942): Scheduling to connect in 5 seconds...

Finally output on mobile is showing loading gif i.e CircularProgressIndicator(), means returns always true at "if (result.isLoading)"

[![enter image description here][2]][2]

Could anyone please help!!

note: the same appsync working perfectly in angular application. [1]: https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html [2]: https://i.stack.imgur.com/59KZh.png

2

There are 2 answers

2
Enphinitie On BEST ANSWER

See my recent update at the bottom.

I'm actually running into the same problem. I don't have a complete answer but I feel like this is a clue to a partial answer.

From the link:

As far as I know, AppSync doesn't use wss://<your_end_point>. I have tried this before, and my http connection was never "upgraded to a wss" connection. I had to POST a request to the http endpoint requesting a subscription connection, then AWS returned connection details containing a new endpoint (a wss url), a topic and a client id. Only then could I connect to the socket using using a third party library (using the URL AWS returned). I had to dig into their official sdk to find this out, and then through trial and error to get it to work.

I've confirmed w/ the GraphiQL tool that I do get a response when I POST my subscription graphql blob. I get a websocket url back along w/ some other items in JSON. I haven't figured out how to capture that response in Flutter yet but I'll update this if I find that piece as well.

From there, I believe we'd need to dynamically create the websocket from that new url and send our subscription graphql blob to that.

Absolutely, this is convoluted but it's the path I'm proceeding with for now.

UPDATE: AWS Amplify for Flutter now supports subscriptions! I am using a hybrid solution. I have flutter_graphql and artemis for mutations/queries. For subscriptions I am using amplify_api: '<1.0.0'. It was just released in the past weeks. It actually only took me a few minutes to get it working. Amplify automatically does the URL resolution and authentication for you.

Link to official docs

You'll need to generate an amplifyconfiguration.dart that is specific for your AWS endpoint and add it to your project. The AWS docs cover this and the person that setup your AWS endpoint should know exactly what you need.

Example from link:

try {
    String graphQLDocument = '''subscription OnCreateTodo {
        onCreateTodo {
          id
          name
          description
        }
      }''';

    var operation = Amplify.API.subscribe(
        request: GraphQLRequest<String>(document: graphQLDocument),
        onData: (event) {
          print('Subscription event data received: ${event.data}');
        },
        onEstablished: () {
          print('Subscription established');
        },
        onError: (e) {
          print('Subscription failed with error: $e');
        },
        onDone: () {
          print('Subscription has been closed successfully');
        });
} on ApiException catch (e) {
    print('Failed to establish subscription: $e');
}

Don't forget to unsubscribe:

// Cancel the subscription when you're finished with it
operation.cancel();
2
micimize On

The first thing I would try here is using @JLuisRojas's custom AppSync request serializer:

class AppSyncRequest extends RequestSerializer {
  final Map<String, dynamic> authHeader;

  const AppSyncRequest({
    this.authHeader,
  });

  @override
  Map<String, dynamic> serializeRequest(Request request) => {
    "data": jsonEncode({
      "query": printNode(request.operation.document),
      "variables": request.variables,
    }),
    "extensions": {
      "authorization": this.authHeader,
    }
  };
}

// ...

final token = session.getAccessToken().getJwtToken();

String toBase64(Map data) => base64.encode(utf8.encode(jsonEncode(data)));

final authHeader = {
  "Authorization": token,
  "host": "$apiId.appsync-api.$zone.amazonaws.com",
};

final encodedHeader = toBase64(authHeader);

final WebSocketLink wsLink = WebSocketLink(
  'wss://$apiId.appsync-realtime-api.$zone.amazonaws.com/graphql?header=$encodedHeader&payload=e30=',
  config: SocketClientConfig(
    serializer: AppSyncRequest(authHeader: authHeader),
    inactivityTimeout: Duration(seconds: 60),
  )
);

final AuthLink authLink = AuthLink(
  getToken: () => token,
);

final Link link = authLink.concat(wsLink);

Notice that not only does the connection seem to require the header query parameter, but it also requires authorization in the extensions field on each request, and a non-standard encoding structure (docs).