Closed AlanChauchet closed 3 years ago
I don't think this is a bug with this library. Unless recently changed, connecting to AppSync subscriptions through third-party gql libraries is not straightforward.
As far as I know, AppSync doesn't use wss://
And now I realise that maybe this library doesn't even work with AppSync (for subscriptions) at all. AppSync seems to have its own weird implementation. Using the url, the client and the array of topics which means you might have to use the websocket library this library is using under the hood, and pass in the response you get from AWS to connect.
If you want to have a go, I've written a class that does this. I'm not sure if it would help your particular case, but it's worth a shot. It's in C#, but converting it to Dart should be relatively straightforward.
Create an http request to your endpoint, where the content is your query, your operation name and your variables:
private StringContent PrepareSubscriptionRequest(string query, string operationName, Dictionary<string, object> variables) {
var contentDictionary = new Dictionary<string, object> {
{ nameof(query), query },
{ nameof(operationName), operationName },
{ nameof(variables), variables }
};
var content = JsonConvert.SerializeObject(contentDictionary);
return new StringContent(content);
}
POST
that to your endpoint (make sure your request headers have the api key under header name x-api-key
, and content type of application/json
) :
private async Task<Subscription> SubscribeAsync(StringContent subscriptionRequest) {
SubscriptionIntermediateResponse intermediateResponse = null;
try {
var response = await _httpClient.PostAsync(Consts.CONFIG_APPSYNC_END_POINT, subscriptionRequest);
if (response.IsSuccessStatusCode) {
var responseContentString = await response.Content.ReadAsStringAsync();
intermediateResponse = JsonConvert.DeserializeObject<SubscriptionIntermediateResponse>(responseContentString);
}
else {
Logger.LogError(response.ReasonPhrase);
}
}
catch (Exception e) {
Logger.LogError(e);
}
if (intermediateResponse?.data is null) {
return new Subscription() { Success = false };
}
string mqttConnection = intermediateResponse.extensions.subscription.mqttConnections[0].url;
string[] topics = intermediateResponse.extensions.subscription.mqttConnections[0].topics;
string client = intermediateResponse.extensions.subscription.mqttConnections[0].client;
return new Subscription() {
MqttWssConnectionString = mqttConnection,
Client = client,
Topics = topics,
Success = true
};
}
If successful, AWS will return a JSON object that contains a wss URL, an array of topics, and a client id, for you to connect with using a ws client:
private IMqttClientOptions MakeOptions(Subscription subscription) {
return new MqttClientOptionsBuilder()
.WithWebSocketServer(subscription.MqttWssConnectionString)
.WithClientId(subscription.Client)
.WithTls()
.WithProtocolVersion(MQTTnet.Serializer.MqttProtocolVersion.V311)
.WithCleanSession()
.Build();
}
I also started out with AppSync, but then a few months later ditched it and set up my own apollo server on an ec2 instance. AppSync had too many restrictions, for my needs anyway. Also often times things would just not work, without any explanation:
chatRoomId
as a filter).This was my experience back in 2018, they might have updated it. I'd suggest you check and make sure all the features you need are supported, before you invest time into making AppSync work.
Here's another thread on AppSync subscriptions: https://github.com/zino-app/graphql-flutter/issues/209
idk anything about AppSync but it seems like they use a different protocol from websockets for subscriptions
The protocol is described here Building a Real-time WebSocket Client.
I am stuck in how I can create Operation for subscription registration. It should be like this:
{
"id": "ee849ef0-cf23-4cb8-9fcb-152ae4fd1e69",
"payload": {
"data": "{\"query\":\"subscription onCreateMessage {\\n onCreateMessage {\\n __typename\\n message\\n }\\n }\",\"variables\":{}}",
"extensions": {
"authorization": {
"Authorization": "xxx",
"host": "example1234567890000.appsync-api.us-east-1.amazonaws.com"
}
}
},
"type": "start"
}
This operation:
Operation(
documentNode: gql("subscription onCreateMessage { onCreateMessage { __typename message } }"),
variables: <String,dynamic> {
'var': 'value'
},
extensions: <String,dynamic> {
"authorization": {
"Authorization": "xxx",
"host": "example1234567890000.appsync-api.us-east-1.amazonaws.com"
}
}
)
produces the message
{
"type": "start",
"id": "8e83b832-16f5-4c43-af47-7ff24b058414",
"payload": {
"operationName": "onCreateMessage",
"query": "subscription onCreateMessage {\n onCreateMessage {\n __typename\n message\n }\n}",
"variables": {
"var": "value"
}
}
}
The payload is different what AppSync expects. There should be fields "data" and "extensions". "data" is clearly combination of "query" and "variables", "extensions" is missing and "operationName" is extra.
I didn't find a way to accomplish this, but I am not familiar with the library, so I would appreciate if someone could point me to right direction?
@jtn-devecto on the v4
alpha you can provide a custom RequestSerializer to the SocketClientConfig you pass to WebSocketLink
. I don't remember how you would do so with v3
so def upgrade to 4.0.0-alpha.7
(it is very stable)
@micimize thanks for the tip. For v3 I end it up writing own link.
@jtn-devecto that's excellent! If you could put it in a gist I might have time to generalize and port it to v4
I am having same problem just i am using Apollo GraphQL subscriptions package with Redis hosted on heroku. Any ideas how to configure server to send keepAlive messages? I am thinking about this:
this.pubsub = new RedisPubSub({
publisher: new Redis(process.env.REDIS_URL, { keepAlive: 10000 }),
subscriber: new Redis(process.env.REDIS_URL, { keepAlive: 10000 }),
});
we are having a similar issue here. Would be great to have a working solution using this package for subscriptions.
After some reading I got it to work. I implemented a custom RequestSerializer as @micimize commented and added a query string to the AWS AppSync endpoint containing the authorization parameters as described here Building a Real-time WebSocket Client.
This is the custom RequestSerializer:
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,
}
};
}
And this is the client code:
const apiId = "example1234567890000";
const zone = "us-west-1";
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);
final client = GraphQLClient(
link: link,
cache: GraphQLCache(),
alwaysRebroadcast: true
);
client.subscribe(SubscriptionOptions(
document: gql("""
subscription SubPost {
addedPost {
id
title
}
}
"""
))).listen((result) {
print("Subscription data:");
print(result.data);
});
Hi, I'm also facing appsync subscription issue in flutter, which is working perfectly in angular application. and my issue explained clearly here , please have a look.
I am having this issue I use GCP cloud run, and am running into the same issue.
I'm going to close this as between https://github.com/zino-app/graphql-flutter/issues/682#issuecomment-759078492 and the s/o answer on amplify there seem to be solutions. If anyone who understands this better wants I would encourage a PR on updating the docs on AppSync Support.
After some reading I got it to work. I implemented a custom RequestSerializer as @micimize commented and added a query string to the AWS AppSync endpoint containing the authorization parameters as described here Building a Real-time WebSocket Client.
This is the custom RequestSerializer:
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, } }; }
And this is the client code:
const apiId = "example1234567890000"; const zone = "us-west-1"; 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); final client = GraphQLClient( link: link, cache: GraphQLCache(), alwaysRebroadcast: true ); client.subscribe(SubscriptionOptions( document: gql(""" subscription SubPost { addedPost { id title } } """ ))).listen((result) { print("Subscription data:"); print(result.data); });
Thank you, your solution is working for me. Saved me hours.
After some reading I got it to work. I implemented a custom RequestSerializer as @micimize commented and added a query string to the AWS AppSync endpoint containing the authorization parameters as described here Building a Real-time WebSocket Client. This is the custom RequestSerializer:
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, } }; }
And this is the client code:
const apiId = "example1234567890000"; const zone = "us-west-1"; 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); final client = GraphQLClient( link: link, cache: GraphQLCache(), alwaysRebroadcast: true ); client.subscribe(SubscriptionOptions( document: gql(""" subscription SubPost { addedPost { id title } } """ ))).listen((result) { print("Subscription data:"); print(result.data); });
Thank you, your solution is working for me. Saved me hours.
Can you please ellaborate , from where this session comes ? final token = session.getAccessToken().getJwtToken();
@flutterdev-2021 The session comes from the current authenticated user. For this you can use the amazon_cognito_identity_dart package.
Here is a more detailed example: https://github.com/furaiev/amazon-cognito-identity-dart-2/#for-appsyncs-graphql
Describe the bug Hello, I started an Amplify project within my Flutter app and I am struggling with GraphQL subscriptions. I set up my environment with AppSync and I am able to perform queries and mutations but when trying to subscribe, the socket is instantly connected then disconnected. Here is the output of the console (where {appId} is replaced by my AppSync app id) :
(It doesn't wait for
inactivityTimeout
but instantly gets disconnected)Here is the code that I use to perform the subscription:
Expected behavior If an error is preventing me from connecting to the socket, I should be warned of it. And I am not sure of what could be wrong in what I am doing there but subscription should be working (the subscription has been tested on the AppSync console and is working as expected)