graphql-client
graphql-client copied to clipboard
Could not connect with AWS graphQL client (Appsync APIs)
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());
});
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.
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.
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.
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.
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=
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);
});
Nope, I didn't tell you anything like that.
Please post your NotificationOnUpdateSubscriptionResult
class and the JSON payload you're expecting from your endpoint.
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!
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...
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
Altair is a JavaScript app... I'm sorry, but it seems something else is wrong there. But go ahead.
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
?
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