graphql-client icon indicating copy to clipboard operation
graphql-client copied to clipboard

Could not connect with AWS graphQL client (Appsync APIs)

Open JackPriyan opened this issue 4 years ago • 13 comments

I am trying to implement the same example given below in python with .net and this grahQL-client nuget but I am always getting error that the "server closed the connection without a closing handshake".

https://aws.amazon.com/blogs/mobile/appsync-websockets-python/

The code in above sample is working good in python.

could any please help with this. I am trying to fix from last two week. Not sure its something inside the library or I am not able to use the library correctly.

My code looks like

var header = System.Convert.ToBase64String(Encoding.UTF8.GetBytes("{'host': '--HOST Name here--', 'x-api-key': '--APISYNC KEY HERE--'}"));
var gQL = new GraphQLHttpClient("--WEBSOCKET URL HERE--", new NewtonsoftJsonSerializer());
gQL.HttpClient.DefaultRequestHeaders.Add("header", header);

var request = new GraphQLHttpRequest{Query = "subscription SubscribeToEventComments{ subscribeToEventComments(eventId: 'test'){  content }}",OperationName = "SubscribeToEventComments", Variables = new{}};
IObservable <GraphQLResponse<string>> subscriptionStream = gQL.CreateSubscriptionStream<string>(request, (Exception ex)=>{
Console.WriteLine(ex.ToString());
});
var subscription = subscriptionStream.Subscribe(response =>
{
Console.WriteLine($"user '{Newtonsoft.Json.JsonConvert.SerializeObject(response)}' joined");
},
ex=>{
Console.WriteLine(ex.ToString());
 });

JackPriyan avatar May 29 '20 18:05 JackPriyan

I managed to get it working with Appsync but only for SendQueryAsync calls. I've also not had much luck yet in getting the websocket connection for realtime subscriptions.

dariusjs avatar Jun 10 '20 15:06 dariusjs

Anyone figure this out? I too can query without an issue but get the following error when trying to connect with websockets.

The server returned status code '401' when status code '101' was expected.

symposiumjim avatar Sep 08 '20 21:09 symposiumjim

AppSync seems to use a different endpoint for websocket connections, see here.

You need to create a second instance of GraphQLHttpClient and point that to the correct endpoint. Please upgrade to V3.1.7, previous versions don't handle the wss:// scheme correctly.

rose-a avatar Sep 15 '20 21:09 rose-a

Sorry, I am trying to use the client in csharp and changing the url to wss does not work. I do this;

clientWebsockets = new GraphQLHttpClient("wss://host/graphql", new NewtonsoftJsonSerializer());

clientWebsockets.HttpClient.DefaultRequestHeaders.Add("x-api-key", clientApiKey);
clientWebsockets.HttpClient.DefaultRequestHeaders.Add("host", host);

var onUpdateNotificationReq = new GraphQLRequest
                {
                    Query = @"
                    subscription {
                        onUpdateNotification {
                            id
                            name
                            action
                        }
                    }"
                };

                IObservable<GraphQLResponse<UserJoinedSubscriptionResult>> subOnUpdateNotificationStream = clientWebsockets.CreateSubscriptionStream<NotificationOnUpdateSubscriptionResult>(onUpdateNotificationReq, (ex) =>
                {
// ERROR HERE
                    Log("EX: " + ex.Message + Environment.NewLine);
                });

                subOnUpdateNotification = subOnUpdateNotificationStream.Subscribe(response =>
                {
                    Log("NEVER SHOWS UP");
                });

end up with same error - The server returned status code '401' when status code '101' was expected.

symposiumjim avatar Sep 18 '20 22:09 symposiumjim

The header must be sent as url query parameter as described here: https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html.

The example there is

wss://example1234567890000.appsync-realtime-api.us-east-1.amazonaws.com/graphql?header=eyJob3N0IjoiZXhhbXBsZTEyMzQ1Njc4OTAwMDAuYXBwc3luYy1hcGkudXMtZWFzdC0xLmFtYXpvbmF3cy5jb20iLCJ4LWFtei1kYXRlIjoiMjAyMDA0MDFUMDAxMDEwWiIsIngtYXBpLWtleSI6ImRhMi16NHc0NHZoczV6Z2MzZHRqNXNranJsbGxqaSJ9&payload=e30=

rose-a avatar Sep 19 '20 20:09 rose-a

Are you telling me that I need to create my own websocket requests and cannot use your system for subscriptions? I do not get the error anymore with the below code, but I never see any data.

var headersWs = new Dictionary<string, string>() {
                    { "host", clientHost },
                    { "x-api-key", clientApiKey }
                };

                var sHeadersWs = JsonConvert.SerializeObject(headersWs);
                var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(sHeadersWs);
                var x = System.Convert.ToBase64String(plainTextBytes);
                var uri = clientEndpointWs + "?header=" + x + "&payload=e30=";

                clientWs = new GraphQLHttpClient(uri, new NewtonsoftJsonSerializer());

                await clientWs.InitializeWebsocketConnection();

                var req = new GraphQLHttpRequest
                {
                    Query = @"
                    subscription OnUpdateNotification {
                        onUpdateNotification {
                          id
                          name
                          action
                          createdAt
                          updatedAt
                        }
                      }",
                    OperationName = "OnUpdateNotification",
                };

                subscriptionStream = clientWs.CreateSubscriptionStream<NotificationOnUpdateSubscriptionResult>(req);

                sub = subscriptionStream.Subscribe(response =>
                {
                    Log("Here");
                    Log(response.Data.onUpdateNotification.Name);
                });

symposiumjim avatar Sep 20 '20 17:09 symposiumjim

Nope, I didn't tell you anything like that.

Please post your NotificationOnUpdateSubscriptionResult class and the JSON payload you're expecting from your endpoint.

rose-a avatar Sep 21 '20 06:09 rose-a

public class NotificationType
        {
            public string Id { get; set; }
            public string Name { get; set; }
            public string Action { get; set; }
        }

public class NotificationOnUpdateSubscriptionResult
        {
            public NotificationType onUpdateNotification { get; set; }
        }

sub = subscriptionStream.Subscribe(response =>
                {
                    Log("Here");
                    Log(response.Data.onUpdateNotification.Name);
                }, error =>
                {
                    Log("ERROR");
                    Log(error.Message);
                });

I never get a response, not even an error. Note that when I used an incorrect URI I would get the 401 error so I think the URI is not connecting, but not sure as there are no messages.

Thanks for all your help so far!

symposiumjim avatar Sep 21 '20 17:09 symposiumjim

Looks ok to me. Did you try to create a subscription using a different tool (like Altair)?

Does this work? Please post the subscription query string and response from there...

rose-a avatar Sep 22 '20 07:09 rose-a

I tested with Altair and do not receive any error message nor any data. Thank you for your help but it seems connecting to AppSync is not something via C# is not going to work so I am moving to another code base

symposiumjim avatar Sep 22 '20 17:09 symposiumjim

Altair is a JavaScript app... I'm sorry, but it seems something else is wrong there. But go ahead.

rose-a avatar Sep 22 '20 19:09 rose-a

There are a couple of showstopper issues. I have fixes for them locally but have not yet polished them for a pull-request.

Issue 1 - Set explicit endpoint for WebSocket

The GraphQLHttpClientOptions class needs a WebSocketEndPoint property to explicitly set the WebSocket endpoint for AppSync as it's different from the REST endpoint. There is some additional scaffolding required, but it doesn't belong in GraphQL.Client. Specifically, the WebSocket URI needs to be passed in with query parameters as follows:

var header = new AppSyncHeader {
    Host = "abcedef.appsync-api.us-west-2.amazonaws.com",
    ApiKey = "abcdef"
};

_graphQlClient.Options.WebSocketEndPoint = new Uri($"wss://abcedef.appsync-realtime-api.us-west-2.amazonaws.com/graphql"
    + $"?header={Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(header)))}"
    + "&payload=e30="
);

With AppSyncHeader declared as follows:

class AppSyncHeader {

    //--- Properties ---

    [JsonPropertyName("host")]
    public string Host { get; set; }

    [JsonPropertyName("x-api-key")]
    public string ApiKey { get; set; }
}

Issue 2 - NullReferenceException in GraphQLRequest.GetHashCode()

The GraphQLRequest class implementation expects to always have the Query property set, but the AppSync subscription request looks like this:

{
  "data": {
    "query": "subscription { ... }"
  },
  "extensions": {
    "authoriziation": {
      "Host": "...",
      "ApiKey": "..."
    }
  }
}

A simple fix is to allow Query to be null. However, I think the hash code should be computed over the entire dictionary instead.

public override int GetHashCode()
{
    unchecked
    {
        var hashCode = Query?.GetHashCode() ?? 0;
        hashCode = (hashCode * 397) ^ OperationName?.GetHashCode() ?? 0;
        hashCode = (hashCode * 397) ^ Variables?.GetHashCode() ?? 0;
        return hashCode;
    }
}

Issue 3 - Helper class for authorized AppSync requests

This is not a blocking issue, just a complication. The request needs to be pre-processed to add the correct authorization header.

_graphQlClient.Options.PreprocessRequest = (request, client) =>
    Task.FromResult((GraphQLHttpRequest)new AuthorizedAppSyncHttpRequest(request, _header.ApiKey));

The AuthorizedAppSyncHttpRequest is declared as follows:

class AuthorizedAppSyncHttpRequest : GraphQLHttpRequest {

    //--- Fields ---
    private readonly string _authorization;

    //--- Constructors ---
    public AuthorizedAppSyncHttpRequest(GraphQLRequest request, string authorization) : base(request)
        => _authorization = authorization;

    //--- Methods ---
    public override HttpRequestMessage ToHttpRequestMessage(GraphQLHttpClientOptions options, Client.Abstractions.IGraphQLJsonSerializer serializer) {
        var result = base.ToHttpRequestMessage(options, serializer);
        result.Headers.Add("X-Api-Key", _authorization);
        return result;
    }
}

Disclaimer

For now, I have only used the API key for authentication. However, API keys are really just for development as they expire after a week. I have not yet checked what is needed to support a Cognito authentication flow.

Question

@rose-a Do you have a recommendation for how to add the declarations for AppSyncHeader and AuthorizedAppSyncHttpRequest? Maybe as a sibling assembly, such as GraphQL.Client.AppSync?

bjorg avatar Sep 29 '20 18:09 bjorg

I spoke a bit too soon. Some more changes were needed. I opened a draft pull-request: https://github.com/graphql-dotnet/graphql-client/pull/287

I also created a sample Blazor WebAssembly app to show how it works: https://github.com/bjorg/GraphQlAppSyncTest/blob/main/MyApp/Pages/Index.razor

bjorg avatar Sep 29 '20 23:09 bjorg