Giter VIP home page Giter VIP logo

graphql-ws's People

Contributors

airhorns avatar amshalem avatar benjie avatar dimamachina avatar dotansimha avatar edwardfeng-db avatar enisdenjo avatar falci avatar gilgardosh avatar hongbo-miao avatar jeevcat avatar kristjanvalur avatar lunawen avatar martinbonnin avatar maxpain avatar michelepra avatar n1ru4l avatar ntziolis avatar rafaelpernil2 avatar roksui avatar rpastro avatar schickling avatar semantic-release-bot avatar shahyar avatar she11sh0cked avatar sibelius avatar spacebarley avatar tbwiss avatar thumbsupep avatar vitaliytv avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql-ws's Issues

WebSocket is closed before the connection is established.

Screenshot
image

image

Expected Behaviour
I expected the connection was created successful.

Actual Behaviour
On localhost, all browsers succeed.
In production,

  • For Firefox, feels 90% time failed. It keeps reconnecting. Tested on Firefox version 79.0, 81.0, and 83.0a1
  • For Chrome, most time succeed, but still failed a couple of times.
  • For Safari, I haven't seen it failed because of this connection error so far.

The errors are like these in Firefox:

Firefox can’t establish a connection to the server at wss://www.hongbomiao.com/graphql. 2.2c591de4.chunk.js:2:134796
The connection to wss://www.hongbomiao.com/graphql was interrupted while the page was loading.

Firefox can’t establish a connection to the server at wss://www.hongbomiao.com/graphql.

or like this in Chrome:

WebSocket connection to 'wss://www.hongbomiao.com/graphql' failed: WebSocket is closed before the connection is established.

Debug Information
Help us debug the bug? Sure.
I added a ping pong subscription demo by hongbo-miao/hongbomiao.com#965
If you open the https://www.hongbomiao.com in Firefox, you will most likely to see the error.

Further Information
This might be helpful
https://stackoverflow.com/questions/14140414/websocket-was-interrupted-while-page-is-loading-on-firefox-for-socket-io

how to debug?

Can we have some logging? like koa logger? like this:

image

I think it would be cool to show:

  • show messages received
  • show messages sent
  • show GraphQL operation info? (operation name ?)

Usage with Relay

Hello,

I'm trying to figure it out how to make this work with Relay. According to Relay docs a subscribe function has to return an observable.

When I write this function:

const subscribe = (request, variables) => {
  const subscribeObservable = subscribeClient.subscribe({
    query: request.text,
    operationName: request.name,
    variables,
  })

  // Important: Convert subscriptions-transport-ws observable type to Relay's
  return Observable.from(subscribeObservable);
}

It just doesn't works, because RelayNetwork returns error: No data returned for operation.

On other hand, when I use subscription-transport-ws it just works.

Interoperability suggestions

Hi!

First of all thanks a lot for taking over this and moving GraphQL over web sockets forward, I believe it's quite important to get some consistency here 🎉 !

I was playing around with this a bit and built an alternate server implementation of the protocol, with the goal of supporting @apollo/client out of the box, with no changes on the client side. I found that there's only a few changes that would allow the latest @apollo/client:

  • Allow both graphql-transport-ws and graphql-ws sub-protocols (the latter is used with @apollo/client)
  • Allow type: "start" in addition to type: "subscribe" for Subscribe messages
  • Allow type: "stop" in addition to type: "complete" for client-to-server Complete messages
  • Use type: "data" instead of type: "next" for Next messages if socket.protocol === "graphql-ws"
  • Allow the client to send Subscribe messages before receiving ConnectionAck. This was the "hardest" one, as the server needs to defer handling the received Subscribe messages until it actually acknowledges the connection. I resolved this through introducing a promise for the acknowledgment, which the Subscribe handlers await before proceeding.

I believe some degree of legacy protocol handling could drive adoption a lot. If we had a server-side GraphQL-over-WS implementation that caters to all popular clients right now, that would be really cool (and graphql-ws could be just that!).

This does not necessarily have to be reflected in the spec and may just go to the implementation. Still, I could imagine adding something like:

  • Servers MAY accept Subscribe messages before a client received the ConnectionAck message in order to support legacy clients.
  • Servers MUST NOT handle Subscribe messages before sending the ConnectionAck message.
  • etc.

I did write an alternate implementation from scratch (and this was quite easy thanks to your work on the protocol) but I could easily provide a PR for the above if you're interested. Let know what you think :)

All the best
Morris

Unable to get subscriptions working with Urql

Hi I'm having an issue getting subscriptions to function with Urql, I am using a class created using examples in docs

class WebSocketLink {
  private client: Client;
  constructor(config: ClientOptions) {
    this.client = createClient(config);
  }
  public request(op: SubscriptionOperation): Observable<ExecutionResult> {
    return new Observable((sink) => {
      return this.client.subscribe<ExecutionResult>(
        { query: op.query, variables: op.variables as Record<string, unknown> | null | undefined },
        {
          ...sink,
          error: (err) => {
            console.log('Error: ', err);
            if (err instanceof Error) {
              sink.error(err);
            } else if (err instanceof CloseEvent) {
              sink.error(new Error(`Socket closed with even ${err.code}` + err.reason ? `: ${err.reason}` : ''));
            } else {
              sink.error(new Error(err.map(({ message }) => message).join(',')));
            }
          },
        },
      );
    });
  }
}

Below is what Urql passes to client.subscribe of which I extract query and variables and pass them on.

export interface SubscriptionOperation {
    query: string;
    variables?: object;
    key: string;
    context: OperationContext;
}

When calling a subscription via Urql I get an error with code: 1002 which appears to be a low level Websocket protocol violation from what I can find. Comparing what is sent to the server from subscriptions-transport-ws and graphql-transport-ws there doesn't appear to be a difference. I am authenticating connections via connectionParams.

I use Express/Postgraphile on the server which uses subscriptions-transport-ws, I am aware you have a pull request to replace subscriptions-transport-ws with your implementation, so unsure if this is a compatibility issue, an error on my part or something else.

Many thanks.

document how to use persisted queries

Story

I want to be able to only send an id instead of a full GraphQL document to the server and then read the actual GraphQL document from a hash table (maybe even asynchronous).


Edit: #36 showed that this is already possible. It just needs to be documented.

In my Socket.io GraphQL layer I choose to omit passing a context, schema, execute, subscribe etc parameters on a server factory level, but instead choose that a getParameter function must be passed to the server factory. That function is invoked for each GraphQL operation executed with the socket and the sent payload. The user can then decide to map the provided GraphQL Payload to anything else and providing a different context/schema/execute/subscribe based on that.

It has the following type signature

export type GetParameterFunctionParameter = {
  socket: SocketIO.Socket;
  graphQLPayload: {
    source: string;
    variableValues: { [key: string]: any } | null;
    operationName: string | null;
  };
};

export type GetParameterFunction = (
  parameter: GetParameterFunctionParameter
) => PromiseOrPlain<{
  graphQLExecutionParameter: {
    schema: GraphQLSchema;
    contextValue?: unknown;
    rootValue?: unknown;
    // These will overwrite the initials if provided; Useful for persisted queries etc.
    operationName?: string;
    source?: string;
    variableValues?: { [key: string]: any } | null;
  };
  execute?: typeof execute;
  subscribe?: typeof subscribe;
}>;

Which then allows doing stuff like this:

import socketIO from "socket.io";

const persistedOperations = {
    "1": "query { ping }"
    "2": "mutation { ping }"
}

const socketServer = socketIO();

const graphqlServer  = registerSocketIOGraphQLServer({
  socketServer,
  getParameter: ({ socket, graphQLPayload }) => ({
    /* The paramaters used for the operation execution. */
    graphQLExecutionParameter: {
      schema,
      rootValue:,
      contextValue: {
        socket,
      },
      // client source is just the id instead of a full document.
      // we map the id to the actual document.
      source: persistedOperations[graphQLPayload.source]
    },
  }),
});

(This design could also address #36 as the getParameter function could throw an error or return a rejected Promise etc).

graphql-transport-ws might get some inspiration from this.

tests are failing on windows and LTS node v12.19.0

I recently got a windows machine and the tests are failing when executed.

FAIL  src/tests/server.ts
  ● Connect › should not close the socket after the `connectionInitWaitTimeout` has passed but the callback is still resolving

    expect.assertions(2)

    Expected two assertions to be called but received one assertion call.

      268 | 
      269 |   it('should not close the socket after the `connectionInitWaitTimeout` has passed but the callback is still resolving', async () => {
    > 270 |     expect.assertions(2);
          |            ^
      271 | 
      272 |     await makeServer({
      273 |       connectionInitWaitTimeout: 10,

      at Object.<anonymous> (src/tests/server.ts:270:12)

  ● Connect › should close the socket if an additional `ConnectionInit` message is received while one is pending

    expect.assertions(3)

    Expected three assertions to be called but received four assertion calls.

      298 | 
      299 |   it('should close the socket if an additional `ConnectionInit` message is received while one is pending', async () => {
    > 300 |     expect.assertions(3);
          |            ^
      301 | 
      302 |     await makeServer({
      303 |       connectionInitWaitTimeout: 10,

      at Object.<anonymous> (src/tests/server.ts:300:12)

 FAIL  src/tests/client.ts
  ● "concurrency" › should dispatch and receive messages even if one subscriber disposes while another one subscribes

    expect(jest.fn()).toBeCalled()

    Expected number of calls: >= 1
    Received number of calls:    0

      399 |     expect(nextFnForHappy).not.toBeCalled();
      400 |     expect(completeFnForHappy).toBeCalled();
    > 401 |     expect(nextFnForBananas).toBeCalled();
          |                              ^
      402 |   });
      403 | });
      404 | 

      at Object.<anonymous> (src/tests/client.ts:401:30)

  ● lazy › should connect immediately when mode is disabled

    expect(received).toBe(expected) // Object.is equality

    Expected: 1
    Received: 2

      411 |     await wait(10);
      412 | 
    > 413 |     expect(server.webSocketServer.clients.size).toBe(1);
          |                                                 ^
      414 |     server.webSocketServer.clients.forEach((client) => {
      415 |       expect(client.readyState).toBe(WebSocket.OPEN);
      416 |     });

      at Object.<anonymous> (src/tests/client.ts:413:49)

Test Suites: 2 failed, 2 total
Tests:       4 failed, 1 todo, 37 passed, 42 total
Snapshots:   0 total
Time:        4.138 s
Ran all test suites.

Subscriptions aren't re-submitted properly on reconnection after connection failures

Screenshot
First websocket and its related subscriptions

image

Next valid connection with its related subscriptions:

image

As you can see, the second screenshot has much less subscriptions sent to the server.

Expected Behaviour

No subscriptions are lost while reconnecting to a server

Actual Behaviour

It managed to reconnect to the server properly but didn't send the whole set of subscriptions

Debug Information

This seems to happen only when the websockets aren't able to reconnect right away to a server.
In my case: I restarted my server locally and because it's using typescript and transpiling on the fly with babel, it takes a little while to restart properly. So to reproduce I just launched my client, my server and then restarted my server and waited.

I am joining a har file below that has content as well, you can open this in any Chromium browser (sadly Firefox doesn't handle HAR with content yet) by dragging the file into your dev tools network panel.

It should load the whole story and you would be able to see my network at the moment of the bug happening.

websockets-operations-after-reconnection.har.zip

Further Information

As with the previous issue, please let me know if you need more information and/or help 🙏

Thank you again for your great work on this new library!

Silent auto-reconnect

Currently, if the socket gets closed for whatever reason, all sinks will immediately complete or error out (depending on the close event).

A much better UX is to silently retry connecting for X amount of times and complete/error out the sink only if the allowed retry count has been exceeded.

Subscriptions aren't re-submitted properly on reconnection

like #85 i have same behavior

i see this happend when websocket stay in CONNECTING state more than 2750ms

Connect function when websocket state is CONNECTING call itself

This cycle is limited to 10 times to prevent many recursive calls and at every call wait for 50*callDepth ms

So if websocket state is CONNECTING for more than 2750ms connect thow this error
image

image

Subscription promise catch this error so this mean that subscription message never send when websocket state switch to CONNECTED

I also note that if sync object is wrapped with observable library like rxjs error throw here will call unsubscribe that definetly stop subscription life

import { Observable } from 'rxjs'
import { Client, SubscribePayload } from 'graphql-ws'
function subscribe(client: Client, gql: SubscribePayload) {
  return new Observable(subscriber => {
    const unsubscribe = client.subscribe(gql, {
      next: subscriber.next.bind(subscriber),
      error: subscriber.error.bind(subscriber),
      complete: subscriber.complete.bind(subscriber),
    })
    return () => unsubscribe()
  })
}

Support async Context function

Story

When a subscription comes in, I'd like to set the context that the resolver receives.
This can be done using the context option of createServer.
Right now, whatever is returned by this function is passed as the context to all resolvers.
I'd like the option to return a Promise, and have the returned value be awaited upon, so that all my resolvers don't need to await the context.

Couldn't connect from the graphql playground

Screenshot
Screen Shot 2020-12-21 at 12 14 46 PM

Expected Behaviour
I should be able to create a subscription on the client (playground). I'm using Apollo Server.

Actual Behaviour
Graphql queries are working but subscription is not.

Debug Information

import { useServer } from 'graphql-ws/lib/use/ws';
import { execute, subscribe } from 'graphql';
import ws from 'ws';


const mockChatServer = new ApolloServer({
  schema: mockSchema,
});

const app = express();
app.use(bodyParser.json());
mockChatServer.applyMiddleware({ app });

const server = createServer(app);
const wsServer = new ws.Server({
  server,
  path: '/graphql',
});

server.listen(3002, () => {
  useServer(
    {
      execute,
      subscribe,
      roots: chatResolvers,
      schema: mockSchema,
      onConnect: ctx => {
        console.log('onconnect');
        return true;
      },
      context: {
        name: 'cool',
      },
      onSubscribe: () => {
        console.log('onsubscribe');
      },
    },
    wsServer,
  );
});

Further Information
None of the console logs about are being called. This is my first time trying to use graphql subscription so I might be making a silly mistake. The subscription endpoint (/graphql) is same as graphql query endpoint but that should not be a problem.
Thank you so much for writing this library. It'd be great if there was a working example.

Add ConnectionNack message to protocol

Story

As a developer I want a message type so that I can inform clients of the reason for the denial of their connection request.

As a client I want to know why my ConnectionInit request was not accepted so that I can fix the issue (e.g. prompt user for auth data) rather than have the connection suddenly close.

Acceptance criteria

  • A developer is able to inform the client why a connection request was denied (for example invalid authentication data)

Implementation-independent type of the query field in the subscribe message

Hello,
we have been looking for an alternative protocol to subscriptions-transport-ws and found this repository which looks very promising and far more mature.

The only problem is the query type in the subscribe message that seems to be javascript specific as the DocumentNode is part of internal classes of graphql-js, and our server is written in Java. Do you plan to make the protocol more open for other languages that use only the string query representation of GQL query in GQL library public API?

Recipe `Client usage with Observable` cause UnhandledPromiseRejectionWarning for rxjs Observables

Expected Behaviour
Recipe Client usage with Observable working.

Actual Behaviour
at subscription.unsubscribe() I get (node:14584) UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'isStopped' of undefined.
Using graphql-ws v4.1.2, rxjs 6.6.3 and node 12.19

Debug Information
Try out Recipe Client usage with Observable

Further Information
Modifying the examples toObservable(..) to

  function toObservable(operation) {
    return new Observable((observer) => client.subscribe(operation, {
      next: observer.next.bind(observer),
      complete: observer.complete.bind(observer),
      error: (err) => observer.error(err)
    }
    ));
  }

solves the problem and makes a working rxjs Observable for subscription.

Keep socket alive for a configured time

Currently, if the ClientOption lazy is true, the websocket is closed as soon as there are no pending operations.

This adds a lot of overhead in case you have an application that is often sending queries or mutations since the client needs to re-establish the socket for every query/mutation.

It would be great if we could add a new keepalive parameter to ClientOptions that would control how long the socket should remain open after the last subscription has completed.

For instance:

export interface ClientOptions {
...
  /**
   * How long the client should wait, after the last query/mutation/subscription is completed, before closing the socket.
   *
   * @default 0 * 1000 (0 seconds)
   */
  keepalive?: number;
...
}

socket.removeEventListener('close',..) not present in 4.1.2, (maybe) reason for MaxListenersExceededWarning from Node

Expected Behaviour
flawless loop query from this most excellent gql websocket library.

Actual Behaviour
Using v4.1.2 iteration 8 of a loop query cause
(node:20284) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 close listeners added to [WebSocket]. Use emitter.setMaxListeners() to increase limit in node v12.19

Debug Information
It seems change https://github.com/enisdenjo/graphql-ws/blame/c4a3c2326192e6a01db08fba49dc5abe6b119c55/src/client.ts#L372 removed the socket.removeEventListener('close',..) previously there, which (maybe?) cause MaxListenersExceededWarning() to emit after using the Client usage with Promise Recipe 8+ times. The MaxListenersExceededWarning is not emitted in graphql-ws v4.1.1.

Further Information
I wrote a simple loop query using the "Client usage with Promise" recipe doing a

  for (const x of Array(15).keys()) {
    const UserLoop = await execute(
      {
        operationName: "user",
        query: '{ user { id } }',
        variables: {},
      });
      console.log("UserLoop",x,"id is", UserLoop?.data?.user?.id)
  }

Using graphql-ws v4.1.1 everything runs smoothly.
Using graphql-ws v4.1.2 MaxListenersExceededWarning: is emitted after 8th' loop.

Perhaps I am doing something wrong?

How to use in GraphQL.js GraphQLSchema object way?

This is my client:

const client = createClient({
  url: 'wss://localhost:5000/graphql',
});

client.subscribe(
  {
    query: 'subscription { greeting }',
  },
  {
    next: (data) => {
      console.log('data', data);
    },
    error: (error) => {
      console.error('error', error);
    },
    complete: () => {
      console.log('no more greetings');
    },
  }
);

I succeed by GraphQL Schema Definition Language way just like the demo in readme (I only changed greetings to greeting). Got 101 Switching Protocols. And the client will print out the greeting five times in the browser console.

Server (Succeed version)

const schema = buildSchema(`
  type Subscription {
    greeting: String
  }
`);

// The roots provide resolvers for each GraphQL operation
const roots = {
  subscription: {
    greeting: async function* sayHiIn5Languages() {
      for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
        yield { greeting: hi };
      }
    },
  },
};

createServer(
  {
    schema,
    execute,
    subscribe,
    roots,
  },
  {
    server,
    path: '/graphql',
  }
);

However, I failed on GraphQL.js GraphQLSchema object way. Got 101 Switching Protocols. But the client never prints out the greeting in the browser console.

Server (Failed version)

import { execute, subscribe, GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql';
import { createServer } from 'graphql-transport-ws';

const GreetingGraphQLType = new GraphQLObjectType({
  name: 'Greeting',
  fields: {
    greeting: { type: GraphQLString },
  },
});

const subscription = new GraphQLObjectType({
  name: 'Subscription',
  fields: {
    greeting: {
      type: GreetingGraphQLType,
      resolve: async function* sayHiIn5Languages() {
        for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
          yield { greeting: hi };
        }
      },
    },
  },
});

const schema = new GraphQLSchema({
  subscription,
});

createServer(
  {
    schema,
    execute,
    subscribe,
  },
  {
    server,
    path: '/graphql',
  }
);

Any idea? Thanks

missing exports in browser bundle

UMD file has missing exports that is declared in *d.ts file

When using module bundler for browser like webpack with web target that honor

"browser": "lib/client.js",

resulting bundle cannot be execute if using exported code other than createClient method

For example using enum MessageType in application code, the bundle resulting buggy in runtime due to missing exported MessageType in graphql-ws umd

To fix just re-export in client.ts or change entry point for rollup from ./src/client.ts to ./src/index.ts

Malfunctioning code in Client usage with Apollo doc

In readme file, section "Client usage with Apollo".
The spread operator on sink object does not work for me. The client throws an error that the next function is undefined. Assigning the properties explicitly works (see the code snippet).
I wonder why is this happening, probably some prototype-related magic that is not clear to me.

public request(operation: Operation): Observable<FetchResult> {
    return new Observable((sink) => {
      return this.client.subscribe<FetchResult>(
        { ...operation, query: print(operation.query) },
        {
          // ...sink,
          next: sink.next,
          complete: sink.complete,

          error: (err) => {
            if (err instanceof Error) {
              sink.error(err);
            } else if (err instanceof CloseEvent) {
              sink.error(
                new Error(
                  `Socket closed with event ${err.code}` + err.reason
                    ? `: ${err.reason}` // reason will be available on clean closes
                    : '',
                ),
              );
            } else {
              // GraphQLError[]
              sink.error(
                new Error(err.map(({ message }) => message).join(', ')),
              );
            }
          },
        },
      );
    });
  }

Subscription with Hasura Server

Actual Behaviour

Hello,

I have the error :
WebSocket connection to 'the url' failed: Error during WebSocket handshake: 'Sec-WebSocket-Protocol' header value 'graphql-ws' in response does not match any of sent values

Debug Information
I work with Hasura on Heroku.
I just interface graphql-ws with Apollo with your code.

Further Information
Everything was working with ws client from Apollo.
But I had to change because of security issues that your kind work strive to resolve.

Optional payload in ConnectionAck

Story

As a server developer I want to pass some connection context to client (for example - current session registration step).

So that client can get it without additional requests.

Acceptance criteria

  • server is able to pass payload in ConnectionAck message (return object in onConnect instead of true?).
  • client is able to handle it properly (pass it in on connected event?).

In subscription-transport-ws it's mentioned in docs, but not implemented in code.

Socket is closed even though there are pending subscriptions

I'm using graphql-ws together with Apollo client.

I noticed that for queries and mutations the cancellerRef.current is being invoked twice. Once when the Complete message is received and again from the cleanup function.

As a result state.locks gets decremented twice for the same operation, which causes the socket to be closed even though there are pending operations.

running code examples

Thanks for making this, it looks awesome

I think it would make it easier and more appealing to have some github repo where you can clone and run to play with this implementation

we could have a monorepo with this matrix of implementations:

server

  • pure http server (not sure if anybody is using this in production)
  • express
  • koa

frontend

  • relay
  • apollo
  • urlq
  • "pure"

integration with GraphiQL-Subscriptions-Fetcher

Story

In order support the adoption of graphql-ws in express-graphql, per our discussion here: graphql/express-graphql#687, I would like to see if graphql-ws client can adopt interface to support the fetcher interface of GraphiQL as defined in here: https://github.com/apollographql/GraphiQL-Subscriptions-Fetcher/blob/master/src/fetcher.ts

Acceptance criteria

  1. has unsubscribe method
  2. subscribe method support the following method signature :
    subscribe ({
    query: graphQLParams.query,
    variables: graphQLParams.variables,
    }, function (error, result) {} )

Ability for server to reject connection with custom reason

Story

As a server developer I want to reject connection with custom reason, based on connection params. e.g. user sends expected api version or app token, and I want to reject with "UpgradeNeeded" or "InvalidToken" reason.

Acceptance criteria

  • server is able to reject user connection with custom reason (return string from onConnect?.. Not sure about code, maybe also it worth return [number, string] for custom code).
  • client is able to handle it properly (already possible with closed callback).

Recommended method of sending a bearer token

We would like to send a bearer token with all of our websocket messages. I know we can add this underneath payload, but this seems more like metadata to me and might fit nice at the top level (next to type and payload).

What do people think of adding an optional authorization key at the top level (or perhaps a way for users to add in custom top level keys?).

I'm thinking something like:

{
   "type":"connection_init",
   "authorization":"BEARER_TOKEN_HERE"
}
{
   "id":"4a4ef089-b43c-447f-9746-0de97f4753c9",
   "type":"subscribe",
   "authorization":"BEARER_TOKEN_HERE",
   "payload":{
      "query":"subscription ...",
      "variables":{
         "..."
      },
      "operationName":"..."
   }
}

Retrying with exponential back-off and randomisation

Story

As a client

I want to use exp back-off and randomisation on retries

So that I prevent the "thundering herd" problem and avoid DDOSing the server when it restarts

Acceptance criteria

  • Client uses the exp back-off for retries
  • Client randomises the initial timeout on retries

Allow client timeout to be specified by the user so it can be set to a higher value than 5 seconds

Story

As a user I'm sometimes opening subscriptions to servers that may need to authenticate some form of credentials which could be dependant on other services

I want to be able to specify a longer timeout before the connection is closed while waiting for the ConnectionAck method

So that I don't fail to connect when authentication to the server takes longer.

Acceptance criteria

  • client is able to set the connection timeout during client creation

should the client be notified that the server schema is missing?

Story

I just checked the test cases and noted that the server sends information to the client that the schema is unavailable.
I have concerns that this information should not be leaked to clients in a non-development environment. The client should rather get an unexpected error occurred type of message.

Issues and security implications with subscriptions-transport-ws

apollographql/subscriptions-transport-ws

The following issue collection is only up until December 2018

🚨 Possible security implications

🛠 Issues

Error message while creating subscription lost due to socket.close() maximum payload size

Screenshot

(node:48769) UnhandledPromiseRejectionWarning: RangeError: The message must not be greater than 123 bytes
    at Sender.close (/Users/jonahss/Workspace/cloud-platform/api/node_modules/graphql-ws/node_modules/ws/lib/sender.js:120:15)
    at WebSocket.close (/Users/jonahss/Workspace/cloud-platform/api/node_modules/graphql-ws/node_modules/ws/lib/websocket.js:233:18)
    at WebSocket.onMessage (/Users/jonahss/Workspace/cloud-platform/api/node_modules/graphql-ws/cjs/server.js:326:28)
    at processTicksAndRejections (internal/process/task_queues.js:89:5)

Expected Behaviour
If an error is thrown, I expect some message about what I did wrong

Actual Behaviour
In this case, graphql-ws tries to send my client the error message from the server, but there is a limit of 123 bytes for the message of a socket close payload. So my client gets this RangeError instead.

Debug Information
This error gets thrown right here:

ctx.socket.close(4400, isProd ? 'Bad Request' : err.message);

Further Information
I guess just truncate the error message? Should probably log the full error plus stacktrace on the server side?

I ran into this error because I returned a rather large object inside my onConnect() handler. This object was not JSON serializable because it contained a cycle. The error warning about the cycle was longer than 123 bytes. Should I create a separate issue for how to handle passing in non-serializable payloads?

Client does not detect React Native WebSocket close event

When a websocket connection is broken (for example, when server restarts), what is the recommended way for the client to reestablish the websocket connection to the server?

I'm using apollo server, and I get the following error on the client side, whenever the server breaks the ws connection:

Object {
  "error": Event {
    "code": 1001,
    "isTrusted": false,
    "reason": "Stream end encountered",
  },
}

In the client websocket link, I can see the error:

class WebSocketLink extends ApolloLink {
  private client: Client;

  constructor(options: ClientOptions) {
    super();
    this.client = createClient(options);
  }

  public request(operation: Operation): Observable<FetchResult> {
    return new Observable((sink) => {
      return this.client.subscribe<FetchResult>(
        { ...operation, query: print(operation.query) },
        {
          next: sink.next.bind(sink),
          complete: sink.complete.bind(sink),
          error: (err) => {
            switch (err?.code) {
              case 1001:
                // error caught here, but how to reconnect?
                break;
            }
            sink.error(err);
          },
        }
      );
    });
  }
}

Currently, I workaround the problem by re-initializing the Apollo Client instance, but I would like to know if there is an official API to restart the socket connection?

Cannot stringify invalid message; isMessage validation for execution result is too strict

A stream payload published by an AsyncIterable returned by execute (from [email protected]) cannot be stringified

{
  id: '7aef51de-707e-49e5-8cbf-df1bff572e5f',
  type: 'next',
  payload: { data: 'Friend', path: [ 'streamTest', 2 ], hasNext: true }
}

It fails silently with the following message, I had to dig into node_modules and add a bunch of console.logs before I had access to this message.

Error: Cannot stringify invalid message
    at Object.stringifyMessage (/Users/laurin/projects/stream-defer-demo/node_modules/graphql-ws/cjs/message.js:87:15)

It would be nice if such an error would not be swallowed by default but instead forwarded to the console. The user could then overwrite the default behavior.

Aside from that, this message object (returned from execute) should be accepted as a valid message. I don't understand why exactly the message sent by the server must be validated - as the server constructs it should be fine? Also, the current validation seems to be rather inflexible and must be updated every time graphql supports a new execution result variation. The responsibility for returning a correct result should be within execute.

Reference code:

type Query {
  greetings: [String]
}
query StreamTestQuery {
  streamTest @stream(initialCount: 1)
}
const Query = new GraphQLObjectType({
  name: "Query",
  fields: {
    streamTest: {
      type: GraphQLList(GraphQLString),
      resolve: async function* () {
        for (const item of ["Hi", "My", "Friend"]) {
          yield item;
          await sleep(1000);
        }
      },
    },
})

ReferenceError: WebSocket is not defined

I've tried running the "Client usage with Apollo" example from the README.md under Node.

Expected Behaviour

  • it works...

Actual Behaviour

ReferenceError: WebSocket is not defined
    at connect (.../node_modules/graphql-transport-ws/lib/client.js:126:24)
    at .../node_modules/graphql-transport-ws/lib/client.js:220:67
    at Object.createClient (.../node_modules/graphql-transport-ws/lib/client.js:244:11)
    ...

Further Information
subscriptions-transport-ws offers an optional webSocketImpl param on the client constructor (https://github.com/apollographql/subscriptions-transport-ws#constructorurl-options-websocketimpl) - any plans to do something similar?

Does this work with express-graphql or replace it?

Thanks @enisdenjo for this awesome package!

A question, does this work with express-graphql or replace it?

Before I had something like

import { graphqlHTTP } from 'express-graphql';

const graphQLMiddleware = graphqlHTTP({
  context: {
    dataLoaders: {
      user: userDataLoader,
    },
  },
  schema,
});

However, when I write code like this, it does not supports context:

import { createServer } from 'graphql-transport-ws';

createServer(
  {
    context: { // <- not support here
      dataLoaders: {
        user: userDataLoader,
      },
    },
    schema,
  },
  {
    server,
    path: '/graphql',
  }
);

Expected HTTP 101 response but was '400 Bad Request'

im following recipes code ( promise ) from https://www.npmjs.com/package/graphql-ws
image

Expected Behaviour
successful handshake with graphql subscription with wss:// protocol

Actual Behaviour
got error "Expected HTTP 101 response but was '400 Bad Request'" and "isTrusted:false"
image

Debug Information
already tested using graphiql and it works fine. Got error when implemented in react native

Further Information
just copy paste the example code. nothing change.
below is my actual code in react native. straight to err ( line-209)
image

image

sorry im a beginner in graphql subscription.

Define execution/subscription `context` during server creation

Story

You want to use an existing contextValue and pass it as a server option during creation like this:

import { execute, subscribe } from 'graphql';
import { createServer } from 'graphql-transport-ws';

// some context which has to be passed in to all execution/subscription operations
const context = { ... };

createServer(
  {
    schema,
    context, // passed in to the operations below
    execute,
    subscribe,
  },
  { ... }
);

Mitigation

This is possible right now, but you have to inject it yourself. Like this:

import { execute, subscribe } from 'graphql';
import { createServer } from 'graphql-transport-ws';

// some context which has to be passed in to all execution/subscription operations
const contextValue = { ... };

createServer(
  {
    schema,
    execute: (args) =>
      execute({
        ...args,
        contextValue, // inject
      }),
    subscribe: (args) =>
      subscribe({
        ...args,
        contextValue, // inject
      }),
  },
  { ... },
);

[RFC] ID uniqueness should be enforced for both Single result and Streaming operation

Story

We have already talked about this once in graphql/graphql-over-http#140 (comment), but I have one confusion.

Let's consider a scenerio of a faulty client (which is usually the culprit for conflict id cases anyway) sending both a mutation and a subscription using the same id.

The client first requests a subscription with id="a". Client does not want to end this subscription yet.

Then the client then sends a mutation with id="a". This is obvious not a problem for the server. However, when the server returns the result of that mutation via type=next,id=a, the client cannot tell if the incoming result is for Subscription with id="a" or Mutation with id="a".

Then the server proceeds to send type=Complete,id=a. However, in this case, it also accidentally makes the client thinks that it is unsubscribed from the the initial subscription query.

documentation of how to handle Authorization

Story

I'm want to be able to send user authorization token that can modify the GraphQL context


When using subscriptions-transport-ws I could generate the GraphQL Context using the onConnect helper

https://github.com/sibelius/relay-workshop/blob/master/packages/server/src/index.ts#L30

import { SubscriptionServer } from 'subscriptions-transport-ws';

SubscriptionServer.create(
    {
      onConnect: async (connectionParams: ConnectionParams) => {
        const { user } = await getUser(connectionParams?.authorization);

        return getContext({ user });
      },
      // eslint-disable-next-line
      onDisconnect: () => console.log('Client subscription disconnected!'),
      execute,
      subscribe,
      schema,
    },
    {
      server,
      path: '/subscriptions',
    },
  );

When using this package, we need to pass context on server creating that we won't be able to modify context based on use authorization token

Server onDisconnect event

Story

As a server I want to track the amount of open websocket connections and update the user's online status when a user disconnects so that I can monitor and analyze the health of my server and update a user's online status immediately when the browser is closed.

Acceptance criteria

  • server can add a disconnect event listener
  • server's disconnect event listener passes either the connection's context or the ctx's extra with request to be able to determine the user

Example use case

import client from 'prom-client'

const webSocketTotalConnectionsCounter = new client.Counter({
  name: 'websocket_connections_total',
  help: 'The total number of Apollo WebSocket connections handled so far.',
})
const webSocketOpenConnectionsGauge = new client.Gauge({
  name: 'websocket_connections_open',
  help: 'The number of open Apollo WebSocket connections.',
})

useServer(
  {
    schema,
    execute,
    subscribe,
    async onConnect(ctx) {
      webSocketTotalConnectionsCounter.inc()
      webSocketOpenConnectionsGauge.inc()

      await setOnline(ctx)
    },
    async onDisconnect(ctx) {
      webSocketOpenConnectionsGauge.dec()

      await setOffline(ctx)
    },
  },
  wsServer,
)

Allow connectionParams to be Promise or async function

Currently the connectionParams option must be either a Record<string, unknown> or a synchronous function that returns Record<string, unknown>.

  /**
   * Optional parameters, passed through the `payload` field with the `ConnectionInit` message,
   * that the client specifies when establishing a connection with the server. You can use this
   * for securely passing arguments for authentication.
   */
  connectionParams?: Record<string, unknown> | (() => Record<string, unknown>);

This is an issue for applications that need to asynchronously retrieve the data for the ConnectionInit message. Like for instance retrieving a security token required for the WS authentication. This is particularly a problem in case lazy option is set to false and the client tries to open the WS immediately.

This is enhancement request to allow connectionParams to be either a Promise or async function, so the application can asynchronously retrieve the information required for the ConnectionInit message.

Client depends on `window` breaking usage in Node environments

Expected Behaviour
Using the client in Node environments works.

Actual Behaviour
The client breaks when invoking the subscribe method.

ReferenceError: window is not defined

    at generateUUID (node_modules/graphql-transport-ws/lib/client.js:367:5)
    at Object.subscribe (node_modules/graphql-transport-ws/lib/client.js:266:26)

Debug Information
Client depends on the browser specific window global object to generate IDs.

Further Information
In order to support the Node environment, the client needs to find the Crypto implementation in the relevant global scope (or maybe even allow passing in a custom generateUUID function?).

improve client tests to avoid wait patten

Story

I'd like to add some tests in our GraphQL subscriptions server to make sure subscriptions are working well, my current problem is that subscribe happens after our pubsub.publish call.

example:

client.subscribe()

pubsub.publish() // this happens before `subscribe` completes

similar to the current test that uses wait(10)
https://github.com/enisdenjo/graphql-transport-ws/blob/f76ac73e9d21c80abe0118007e168e4f5d525036/src/tests/client.ts#L113

does it make sense to emit an event when the subscribe is "ready"

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.