Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

grapqhl-ws/client : malformed query error are swallowed and client simply "completes" #245

Closed
nomocas opened this issue Oct 18, 2021 · 25 comments
Labels
bug Something isn't working released Has been released and published

Comments

@nomocas
Copy link

nomocas commented Oct 18, 2021

Hello All,

Client graphql-ws v5.5.0
on nodejs v14.17.0

Server : Hasura v2.0.9

Expected Behaviour
I expect graphql-ws/client to show an error when a query is malformed.

Actual Behaviour
Instead it simply close the connection (complete event) without providing any clue. So, if you do not have access to server's logs : you don't understand why this behaviour happen.

It shows nothing even if I add all possible events handlers in my client. e.g. :

const client = createClient({
       ....
        on: {
            connected: () => {
                debug('connected');
            },
            connecting: () => {
                debug('connecting');
            },
            opened: () => {
                debug('opened');
            },
            closed: () => {
                debug('closed');
            },
            message: (msg) => {
                debug('message', msg);
            },
            ping: (msg) => {
                debug('ping', msg);
            },
            pong: (msg) => {
                debug('pong', msg);
            },
            error: (err) => {
                console.error(err);
            },
        },
        onNonLazyError: (errorOrCloseEvent) => {
            console.error(errorOrCloseEvent);
        },
    });

Debug Information
To reproduce, simply add a typo in your query/subscription and boom : it "completes" instead of throwing.

Thanks for adding proper error handlers in that case.
Clue : in the catch at line 642 (client.ts) : simply rethrow or at least print the error message.

@enisdenjo
Copy link
Owner

enisdenjo commented Oct 18, 2021

Validation happens on the server, not on the client. Re-throwing the catch on line 642 wont change anything.

Invalid queries are fatal errors; meaning, the connection gets closed immediately and you have the necessary information in the close event. This indeed works and I've added a test case to confirm:

it('should close socket with error on malformed request', async (done) => {
expect.assertions(4);
const { url } = await startTServer();
const client = createClient({
url,
lazy: false,
retryAttempts: 0,
onNonLazyError: noop,
on: {
closed: (err) => {
expect((err as CloseEvent).code).toBe(CloseCode.InternalServerError);
expect((err as CloseEvent).reason).toBe(
'Syntax Error: Unexpected Name "notaquery".',
);
},
},
});
client.subscribe(
{
query: 'notaquery',
},
{
next: noop,
error: (err) => {
expect((err as CloseEvent).code).toBe(CloseCode.InternalServerError);
expect((err as CloseEvent).reason).toBe(
'Syntax Error: Unexpected Name "notaquery".',
);
client.dispose();
done();
},
complete: noop,
},
);
});

To get the message, check the error argument of the on.closed callback.

const client = createClient({
       ....
        on: {
            connected: () => {
                debug('connected');
            },
            connecting: () => {
                debug('connecting');
            },
            opened: () => {
                debug('opened');
            },
-           closed: () => {
-               debug('closed');
-           },
+           closed: (err) => {
+               debug('closed', err.code, err.reason);
+          },
            message: (msg) => {
                debug('message', msg);
            },
            ping: (msg) => {
                debug('ping', msg);
            },
            pong: (msg) => {
                debug('pong', msg);
            },
            error: (err) => {
                console.error(err);
            },
        },
        onNonLazyError: (errorOrCloseEvent) => {
            console.error(errorOrCloseEvent);
        },
    });

@enisdenjo
Copy link
Owner

Can you please share the close code? If its 1000, the issue is related to #244 and is affected by the same problem.

@nomocas
Copy link
Author

nomocas commented Oct 18, 2021

Indeed, we have now a "closed 4400 Invalid message".

but this is not the server message.

When a add a console log in the catch (line 643) :

I see :
{"type":"error","id":"072366df-a63f-45cc-8afb-e5ea46068ef1","payload":{"errors":[{"extensions":{"path":"$.selectionSet.session.selectionSet.received_paper_voucher","code":"validation-failed"},"message":"field "received_paper_voucher" not found in type: 'session'"}]}}

Which is obviously much more related and clear than a "Invalid message".

@enisdenjo
Copy link
Owner

enisdenjo commented Oct 18, 2021

You mean line 643 of the build in the lib/ directory, which file exactly? I am looking at line 643 in the source code's client.ts where re-throwing should not fix the issue.

Setting retryAttempts to zero (0) here, should report the error immediately if the close code is 4400. Can you confirm?

@nomocas
Copy link
Author

nomocas commented Oct 18, 2021

No in the build it's another line as the code have been transpiled before.. but it's the same place.

So in the transpiled client.js file : it's at line 696 where I've added a console.log.

@enisdenjo
Copy link
Owner

If the close code is 4400, can you please confirm that setting retryAttempts to zero (0) reports the error immediately?

@enisdenjo
Copy link
Owner

Receiving close code 4400 should fail regardless of retryAttempts since it is considered fatal. Can you share your subscribe code?

@enisdenjo
Copy link
Owner

I tried sending a malformed query to Hasura Cloud through graphql-ws, fails as expected: immediately. Are you sure you're handling the subscription error callback properly?

@nomocas
Copy link
Author

nomocas commented Oct 18, 2021

It does not change anything with or without retryAttempts:0.

Here is my subscribeQueue implementation (inspired from your recipes about 'Client usage with AsyncIterator').
In my case, I do not need to be able to do a for-await-of, but I need to check the responses (in order) to do some test at different places in my code.
So it's not a pure AsyncIterator, but it looks like. And again, when query is not malformed, everything is working as expected with it.

export function subscribeQueue<T>(client: Client, payload: SubscribePayload) {
    debug('subscribeQueue:', payload);
    let deferred: {
            resolve: (done: boolean) => void;
            reject: (err: unknown) => void;
        } | null = null,
        error: unknown = null,
        done = false;

    const pending: ExecutionResult<T, unknown>[] = [];

    const dispose = client.subscribe<T>(payload, {
        next: (data) => {
            debug('receive data:', data);
            pending.push(data);
            deferred?.resolve(false);
        },
        error: (err) => {
            debug('error:', err);
            error = err;
            deferred?.reject(error);
        },
        complete: () => {
            debug('complete');
            done = true;
            deferred?.resolve(true);
        },
    });

    return {
        pending,

        dispose,

        async next() {
            debug('subscribeQueue.next');
            if (error) {
                throw error;
            }
            if (done) {
                throw new Error('subscription is done but should not!');
            }
            return pending.length
                ? pending.shift()
                : (await new Promise<boolean>((resolve, reject) => {
                      debug('queue : waiting for next elem(s)!');
                      const timeout = setTimeout(() => {
                          debug('!!!!!!!!!!! Subscription Timeout !!!!!!!!!!!!');
                          error = new Error('Subscription Timeout!');
                          deferred = null;
                          reject(error);
                      }, 5000);

                      deferred = {
                          resolve: (res: boolean) => {
                              debug('deferred.resolve');
                              clearTimeout(timeout);
                              resolve(res);
                          },
                          reject: (reason: unknown) => {
                              debug('deferred.reject');
                              clearTimeout(timeout);
                              reject(reason);
                          },
                      };
                  }))
                ? undefined
                : pending.shift();
        },
    };
}

@enisdenjo
Copy link
Owner

Do you get the debug('error:', err); log from client.subscribe? Can you share the logs?

@nomocas
Copy link
Author

nomocas commented Oct 18, 2021

Nope, no error log :

hpk:graphql-ws subscribeQueue: +110ms
hpk:graphql-ws connecting +0ms
hpk:graphql-ws subscribeQueue.next +4ms
hpk:graphql-ws queue : waiting for next elem(s)! +1ms
hpk:graphql-ws opened +10ms
hpk:graphql-ws message { type: 'ping', payload: { message: 'keepalive' } } +3ms
hpk:graphql-ws ping true +0ms
hpk:graphql-ws pong false +0ms
hpk:graphql-ws message { type: 'connection_ack' } +4ms
hpk:graphql-ws connected +0ms
hpk:graphql-ws message { type: 'ping', payload: { message: 'keepalive' } } +0ms
hpk:graphql-ws ping true +0ms
hpk:graphql-ws pong false +0ms
hpk:graphql-ws message { type: 'complete', id: 'cd0ac83b-f94a-40e2-8ae7-e88c77a35e7a' } +10ms
hpk:graphql-ws complete +0ms
hpk:graphql-ws deferred.resolve +1ms
hpk:graphql-ws closed :  code 4400, reason : Invalid message +35ms

@enisdenjo
Copy link
Owner

Any chance for a repro, at least the backend setup? Whatever I try, I cannot replicate this issue. In my tests, the subscribe error callback gets called every time a close happens.

@nomocas
Copy link
Author

nomocas commented Oct 19, 2021

Hello Denis,

Here is the repro stuffs :

/docker-compose.yaml

version: "3.7"
services:
  postgres:
    container_name: pg-test
    image: postgres:13.3-alpine
    restart: always
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: postgrespassword
    ports:
      - "5432:5432"
  graphql-engine:
    container_name: hasura-test
    image: hasura/graphql-engine:v2.0.9
    ports:
      - "8080:8080"
    depends_on:
      - "postgres"
    restart: always
    environment:
      HASURA_PORT: 8080
      HASURA_GRAPHQL_DATABASE_URL: postgres://postgres:postgrespassword@postgres:5432/postgres
      HASURA_GRAPHQL_ADMIN_SECRET: MyReallySuperSecret
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true"
volumes:
  db_data:

/index.ts

import { createClient, SubscribePayload, Client } from "graphql-ws";
import ws, { CloseEvent } from "ws";
import { v4 } from "uuid";
import dbg from "debug";

const debug = dbg("app");

export function createGQLClient(url: string) {
  debug("createGQLClient(%s)", url);
  return createClient({
    url,
    webSocketImpl: ws,
    generateID: () => v4(),
    // retryAttempts: 0, // with or without it
    connectionParams: {
      headers: {
        "x-hasura-admin-secret": "MyReallySuperSecret",
      },
    },
    on: {
      connected: () => {
        debug("connected");
      },
      connecting: () => {
        debug("connecting");
      },
      opened: () => {
        debug("opened");
      },
      closed: (err: unknown) => {
        debug(
          "closed : code %s, reason : %s",
          (err as CloseEvent).code,
          (err as CloseEvent).reason
        );
      },
      message: (msg) => {
        debug("message", msg);
      },
      ping: (msg) => {
        debug("ping", msg);
      },
      pong: (msg) => {
        debug("pong", msg);
      },
      error: (err) => {
        debug("error");
        console.error(err);
      },
    },
    onNonLazyError: (errorOrCloseEvent) => {
      debug("NonLazyError");
      console.error(errorOrCloseEvent);
    },
  });
}

function subscribe(client: Client, payload: SubscribePayload) {
  client.subscribe(payload, {
    next: (data) => {
      debug("receive data:", data);
    },
    error: (err) => {
      debug("error:", err);
    },
    complete: () => {
      debug("complete");
    },
  });
}

subscribe(createGQLClient("ws://localhost:8080/v1/graphql"), {
  query: "subscription foo($bar:String) {  }",
});

/package.json

{
  "name": "test",
  "scripts": {
    "env": "docker compose up",
    "test": "DEBUG=app npx ts-node index.ts"
  },
  "dependencies": {
    "@types/debug": "^4.1.7",
    "@types/uuid": "^8.3.1",
    "@types/ws": "^8.2.0",
    "debug": "^4.3.2",
    "graphql": "^15.6.1",
    "graphql-ws": "^5.5.1",
    "typescript": "^4.4.4",
    "uuid": "^8.3.2",
    "ws": "^8.2.3"
  }
}

Then in a first terminal yarn env which will bootstrap an hasura and its postgres.

Then when env is running, yarn test in another terminal.

Results :

  app createGQLClient(ws://localhost:8080/v1/graphql) +0ms
  app connecting +1ms
  app opened +15ms
  app message { type: 'ping', payload: { message: 'keepalive' } } +2ms
  app ping true +2ms
  app pong false +0ms
  app message { type: 'connection_ack' } +3ms
  app connected +0ms
  app message { type: 'ping', payload: { message: 'keepalive' } } +1ms
  app ping true +0ms
  app pong false +0ms
  app message { type: 'complete', id: 'b338f0db-2a5e-4784-85af-cb17d94c2a7c' } +7ms
  app complete +1ms
  app closed : code 4400, reason : Invalid message +7ms

@nomocas
Copy link
Author

nomocas commented Oct 19, 2021

If we add a console.log at line 190 in graphql-ws/lib/client.js as console.log('socket.onmessage:', data);

  app createGQLClient(ws://localhost:8080/v1/graphql) +0ms
  app connecting +1ms
  app opened +13ms
socket.onmessage: {"type":"ping","payload":{"message":"keepalive"}}
  app message { type: 'ping', payload: { message: 'keepalive' } } +1ms
  app ping true +2ms
  app pong false +0ms
socket.onmessage: {"type":"connection_ack"}
  app message { type: 'connection_ack' } +1ms
  app connected +0ms
socket.onmessage: {"type":"ping","payload":{"message":"keepalive"}}
  app message { type: 'ping', payload: { message: 'keepalive' } } +0ms
  app ping true +0ms
  app pong false +0ms

socket.onmessage: {"type":"error","id":"9dc4eba2-7042-405d-a6dc-4824d7c7f667","payload":{"errors":[{"extensions":{"path":"$.query","code":"validation-failed"},"message":"not a valid graphql query"}]}}

socket.onmessage: {"type":"complete","id":"9dc4eba2-7042-405d-a6dc-4824d7c7f667"}
  app message { type: 'complete', id: '9dc4eba2-7042-405d-a6dc-4824d7c7f667' } +5ms
  app complete +1ms
  app closed : code 4400, reason : Invalid message +6ms

We see the error from server.
After some checks, the problem appears to come from the fact that it's not recognized as a valid message by the graphql-ws/lib/common.js#isMessage function. When we check the constraints expressed in this function we see :

  case MessageType.Error:
     return hasOwnStringProperty(val, 'id') && areGraphQLErrors(val.payload);

(line 250 and 251 in the source file graphql-ws/src/common.ts)
with graphql-ws/utils#areGraphQLErrors as

export function areGraphQLErrors(obj: unknown): obj is readonly GraphQLError[] {
  return (
    Array.isArray(obj) &&
    // must be at least one error
    obj.length > 0 &&
    // error has at least a message
    obj.every((ob) => 'message' in ob)
  );
}

which could not match the received payload :

{
  "type":"error",
  "id":"9dc4eba2-7042-405d-a6dc-4824d7c7f667",
  "payload":{
      "errors":[
          {"extensions":{"path":"$.query","code":"validation-failed"},"message":"not a valid graphql query"}
      ]
  }
}

Your parser is expecting that payload is directly a GraphqlError[], and hasura provides one wich contains a sub properties errors which is the expected array.

So in fact it's not clear who should follow which payload standard.

Maybe you have an opinion ?

Thanks for following this thread.

@nomocas
Copy link
Author

nomocas commented Oct 20, 2021

Maybe the solution is to keep the payload as unknown or maybe T extends Record<string, unknown> if we want to keep the constraint that payload must be an object.

Because, even if it could be valuable to have a strictly typed payload, it's dangerous to be too strict in case of error, as it will swallow important information for debug if it does not match (as for the moment) for any reason.
Instead, maybe you could print a warning when payload doesn't match your standard, but never reject it silently as 'Invalid Message'.

As this, we have all the information and we are able to validate the payload on our side, if necessary.

WDYT ?

@enisdenjo
Copy link
Owner

The problem here, similar to #244, is wrong implementation of the GraphQL over WebSocket Protocol's Error message. See more here: hasura/graphql-engine#7706.

However, thank you for the repro setup, @nomocas, I'll give it a go soon since the subscription should be reporting an error regardless.

@enisdenjo enisdenjo added the bug Something isn't working label Oct 20, 2021
@enisdenjo
Copy link
Owner

enisdenjo commented Oct 20, 2021

Hey @nomocas, there was a bug in the client where a close event was swallowed if the next read message in queue was a Complete message. See more here: 27754b2.

A new release is coming soon, I just want to pack in a few more things. Stay tuned. See: #245 (comment).

Thank you for your insights, they were very helpful!

enisdenjo pushed a commit that referenced this issue Oct 20, 2021
## [5.5.2](v5.5.1...v5.5.2) (2021-10-20)

### Bug Fixes

* **client:** Don't complete after connection error ([5f829c3](5f829c3))
* **client:** Report close error even if `Complete` message followed ([27754b2](27754b2)), closes [#245](#245)
@enisdenjo
Copy link
Owner

🎉 This issue has been resolved in version 5.5.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

@enisdenjo enisdenjo added the released Has been released and published label Oct 20, 2021
@enisdenjo
Copy link
Owner

v5.5.2 is now out! Please update, @nomocas, and try your malformed query again - the close error should be reported properly.

You still want to keep an eye on hasura/graphql-engine#7706, though. The fix there would keep the connection open and report the error gracefully to the specific problematic subscription.

@nomocas
Copy link
Author

nomocas commented Oct 20, 2021

Thanks! It works.

But... :) Could we get the problematic payload somewhere ?

The current injected error is the ws.CloseEvent, which continue to hide the real problem, as we are not able to retrieve the payload (wellformed or malformed, anyway) from it. So, it's not anymore silent, but it still opaque.

Also, if we check the timing of the console here, we see that there is a delay between the error detection (on parsing in socket.onmessage), and the final close event. (around 15 ms in this simple setup - much more on real setup). And we continue to see intermediate events.

So, after all, it would be coherent to have the CloseEvent injected in the close handler, with a message as "Invalid message" if the payload is malformed. But to be accurate and transparent, and to allow in-time management of those kind of error, it would be really great to have another Error object, injected at detection time in error handlers only, that provides the problematic payload as information. As a malformed query is a programmatic error, whatever, we should throw and kill the app, then correct the queries, and restart.

Finally, as you, of course, do not have control on all gql engine, and that errors are often badly managed, it would be risky to keep hiding malformed payload. In other words, it will happen. And if we do not understand what is happening, often we'll ask you. ;)

@enisdenjo
Copy link
Owner

enisdenjo commented Oct 20, 2021

Once Hasura properly implements the Error message with hasura/graphql-engine#7706, you will get the actual validation error gracefully. The "Invalid message" close happens due to improper implementation of the protocol, specifically an invalid message being transmitted (and there is a clause in the protocol exactly describing how to act on invalid messages). I will not change the protocol to accompany malformed messages just because someone does not implement it properly. Please track the issue there.

The "real" graphql-ws library bug in this issue is the swallowed close error, and the fix deployed with 27754b2 will make sure no connection error gets swallowed again.

@nomocas
Copy link
Author

nomocas commented Oct 20, 2021

I do not ask to change the protocol.

A first question : why it's a ws.CloseEvent object which is injected in the client error handlers ? (And not an Error)

If I check the logs order now (v5.5.2), the error handler is triggered after the close handler, in absolute last position. So we have the proper close event object injected (with proper message) and triggered accordingly to the protocol.
And if I check the protocol, I do not find anywhere about the fact that any errors handlers should receive a CloseEvent precisely. IMHO, the protocol is talking about messages format. Not the way a client should handle errors.

If a client connect to a server that, unfortunately, does not implement the protocol correctly, but returns an object which contains information, I really do not see why the client should hide the payload, which is coming from an object that says type:'error' and provides an id, partially accordingly to the protocol.
Particularly if the reason is Invalid Message.
It should allow instant debug, comparatively to digging in servers log, that we often do not have.

@enisdenjo
Copy link
Owner

I do not ask to change the protocol.

The only way to read the malformed error message's payload is to modify the protocol simply because it considers messages not described by the protocol as invalid.

To read the payload we have to make one of the following breaking changes in the protocol:

  • Allow invalid messages
  • Change the Error message structure

why it's a ws.CloseEvent object which is injected in the client error handlers ? (And not an Error)

Because the user should get the first, root most level, error, not a wrap. If the error causer is a CloseEvent, all error handlers should get a CloseEvent.

And if I check the protocol, I do not find anywhere about the fact that any errors handlers should receive a CloseEvent precisely. IMHO, the protocol is talking about messages format. Not the way a client should handle errors.

The protocol should not, and does not, describe how to handle errors either side.

What it does describe is the transport, how it should look like and what the exact format of each message is.

If a client connect to a server that, unfortunately, does not implement the protocol correctly, but returns an object which contains information, I really do not see why the client should hide the payload, which is coming from an object that says type:'error' and provides an id, partially accordingly to the protocol.

Again, if the server, or the client, receives an invalid message - the connection must be terminated.

The lib does not care what the message is, what type, what it holds, or what the context is - if the message is invalid, terminate the connection.

That being said, the client does not even get to the point of interpreting the malformed error message's payload because the connection gets terminated. No handling of messages takes place after the termination, there is no message to handle (because only valid messages are allowed). Keeping this in mind, the first, root most, and only, error here is the CloseEvent.

Once Hasura fixes the format of the error message with hasura/graphql-engine#7706, the connection will not terminate and you will get the focused GraphQLErrors from the error message's payload in the error callback of the relevant subscription.

@nomocas
Copy link
Author

nomocas commented Oct 20, 2021

Yes : Invalid message means termination through CloseEvent in close handler. No problem with that.
Actual hasura payload should trigger CloseEvent('Invalid Message'). true.

And this will still true if the invalid message is provided, by any way, as convenience, to any developer that encounter it.

What is also true, is that Hasura issues take a long time to be tackled, there is 1.4 K open issues at this moment, and we have really few insight about the road-map. Even if it's one of the biggest project around graphql... So, ok, no choice, we'll wait and see.

And so last try, even a debug mode that, when enabled, print what is happening under the hood ?

Whatever, already thanks for your time.

@enisdenjo
Copy link
Owner

I mean, this is OSS, you can always fork the repo, apply your changes, yarn build, yarn pack and in your app yarn add ../graphql-ws/package.tgz. 😄

github-actions bot pushed a commit to ilijaNL/graphql-transport-ws that referenced this issue Mar 16, 2023
# 1.0.0 (2023-03-16)

### Bug Fixes

* Add `browser` export map ([ea306db](ea306db))
* Add `package.json` to exports map ([enisdenjo#119](https://github.com/ilijaNL/graphql-ws/issues/119)) ([1f09863](1f09863)), closes [enisdenjo#118](https://github.com/ilijaNL/graphql-ws/issues/118)
* Add `uWebSockets` exports path ([36247cb](36247cb)), closes [enisdenjo#155](https://github.com/ilijaNL/graphql-ws/issues/155)
* Add support for `graphql@v16` ([ad5aea2](ad5aea2))
* add the sink to the subscribed map AFTER emitting a subscribe message ([814f46c](814f46c))
* Add types path to package.json `exports` ([enisdenjo#375](https://github.com/ilijaNL/graphql-ws/issues/375)) ([9f394d7](9f394d7))
* **client:** `complete` should not be called after subscription `error` ([1fba419](1fba419))
* **client:** `ConnectionInit` payload is absent if `connectionParams` returns nothing ([98f8265](98f8265))
* **client:** `isFatalConnectionProblem` defaults to undefined for using `shouldRetry` ([9d5c573](9d5c573))
* **client:** Accept nullish values for `operationName` and `variables` ([2d60dda](2d60dda))
* **client:** cant read the `CloseEvent.reason` after bundling so just pass the whole event to the sink error and let the user handle it ([9ccb46b](9ccb46b))
* **client:** Close event's `wasClean` is not necessary ([2c65f0e](2c65f0e)), closes [enisdenjo#81](https://github.com/ilijaNL/graphql-ws/issues/81)
* **client:** Close with error message during connecting issues ([f8ecdd7](f8ecdd7))
* **client:** Connection locks dont increment on retries ([1e7bd97](1e7bd97)), closes [enisdenjo#153](https://github.com/ilijaNL/graphql-ws/issues/153)
* **client:** Debounce close by `lazyCloseTimeout` ([c332837](c332837)), closes [enisdenjo#388](https://github.com/ilijaNL/graphql-ws/issues/388)
* **client:** Dispose of subscription on complete or error messages ([enisdenjo#23](https://github.com/ilijaNL/graphql-ws/issues/23)) ([fb4d8e9](fb4d8e9))
* **client:** Distinguish client connection closes ([ed4d9db](ed4d9db))
* **client:** Don't complete after connection error ([5f829c3](5f829c3))
* **client:** Export relevant elements from the browser bundle ([b106dbe](b106dbe)), closes [enisdenjo#97](https://github.com/ilijaNL/graphql-ws/issues/97)
* **client:** Lazy connects after successful reconnects are not retries ([99b85a3](99b85a3))
* **client:** Limit client emitted error close message size ([2d959f6](2d959f6))
* **client:** New `error` event listener for handling connection errors ([enisdenjo#136](https://github.com/ilijaNL/graphql-ws/issues/136)) ([127b69f](127b69f)), closes [enisdenjo#135](https://github.com/ilijaNL/graphql-ws/issues/135)
* **client:** No retries when disposed ([0d5e6c2](0d5e6c2))
* **client:** One cleanup per subscription ([enisdenjo#67](https://github.com/ilijaNL/graphql-ws/issues/67)) ([5a5ae4d](5a5ae4d))
* **client:** Only `query` is required in the subscribe payload ([e892530](e892530))
* **client:** Reduce WebSocket event listeners and add new client `message` event ([enisdenjo#104](https://github.com/ilijaNL/graphql-ws/issues/104)) ([68d0e20](68d0e20)), closes [enisdenjo#102](https://github.com/ilijaNL/graphql-ws/issues/102)
* **client:** Report close causing internal errors to error listeners ([4e7e389](4e7e389))
* **client:** Report close error even if `Complete` message followed ([27754b2](27754b2)), closes [enisdenjo#245](https://github.com/ilijaNL/graphql-ws/issues/245)
* **client:** Return ping's payload through the response pong ([ee6193a](ee6193a))
* **client:** send complete message and close only if socket is still open ([49b75ce](49b75ce))
* **client:** Should emit `closed` event when disposing ([5800de8](5800de8)), closes [enisdenjo#108](https://github.com/ilijaNL/graphql-ws/issues/108)
* **client:** Shouldn't reconnect if all subscriptions complete while waiting for retry ([2826c10](2826c10)), closes [enisdenjo#163](https://github.com/ilijaNL/graphql-ws/issues/163)
* **client:** Shouldn’t send the `Complete` message if socket is not open ([cd12024](cd12024))
* **client:** Some close events are not worth retrying ([4d9134b](4d9134b))
* **client:** Specify and fail on fatal internal WebSocket close codes ([a720125](a720125))
* **client:** Stabilize and simplify internals ([enisdenjo#100](https://github.com/ilijaNL/graphql-ws/issues/100)) ([5ff8f1d](5ff8f1d)), closes [enisdenjo#99](https://github.com/ilijaNL/graphql-ws/issues/99) [enisdenjo#85](https://github.com/ilijaNL/graphql-ws/issues/85)
* **client:** Stop execution if `connectionParams` took too long and the server kicked the client off ([1e94e45](1e94e45)), closes [enisdenjo#331](https://github.com/ilijaNL/graphql-ws/issues/331)
* **client:** Subscribes even if socket is in CLOSING state due to all subscriptions being completed ([3e3b8b7](3e3b8b7)), closes [enisdenjo#173](https://github.com/ilijaNL/graphql-ws/issues/173) [enisdenjo#170](https://github.com/ilijaNL/graphql-ws/issues/170)
* **client:** Subscription can be disposed only once ([abd9c28](abd9c28)), closes [enisdenjo#170](https://github.com/ilijaNL/graphql-ws/issues/170)
* **client:** Subscriptions acquire locks ([eb6cb2a](eb6cb2a))
* **client:** Time retries and socket change waits ([7c707db](7c707db)), closes [enisdenjo#85](https://github.com/ilijaNL/graphql-ws/issues/85)
* **client:** Wait for server acknowledgement indefinitely ([a4bd602](a4bd602)), closes [enisdenjo#98](https://github.com/ilijaNL/graphql-ws/issues/98)
* Close the details tag in the README ([84144c4](84144c4))
* correctly detect WebSocket server ([eab29dc](eab29dc))
* Define entry points through the `exports`  field and use `.mjs` suffixed ESM imports ([enisdenjo#110](https://github.com/ilijaNL/graphql-ws/issues/110)) ([4196238](4196238))
* Define graphql execution results ([a64c91b](a64c91b))
* Drop TypeScript DOM lib dependency ([a81e8c1](a81e8c1))
* export both the client and the server from index ([29923b1](29923b1))
* Export useful types ([e4cc4d4](e4cc4d4))
* **fastify-websocket:** Handle connection and socket emitted errors ([71e9586](71e9586))
* **fastify-websocket:** Handle server emitted errors ([3fa17a7](3fa17a7))
* http and ws have no default exports ([5c01ed9](5c01ed9))
* include `types` file holding important types ([f3e4edf](f3e4edf))
* Main entrypoint in `exports` is just `"."` ([8f70b02](8f70b02))
* **message:** Allow `data` field to be of any type ([533248e](533248e)), closes [enisdenjo#72](https://github.com/ilijaNL/graphql-ws/issues/72)
* **message:** Allow `payload` field to be of any type for `NextMessage` ([7cebbfe](7cebbfe)), closes [enisdenjo#72](https://github.com/ilijaNL/graphql-ws/issues/72)
* Node 10 is the min supported version ([19844d7](19844d7))
* notify only relevant sinks about errors or completions ([62155ba](62155ba))
* Only UMD build has side effects ([66ed43f](66ed43f))
* Reorder types paths in package.json for better import resolution ([enisdenjo#406](https://github.com/ilijaNL/graphql-ws/issues/406)) ([37263c5](37263c5))
* reset connected/connecting state when disconnecting and disposing ([2eb3cd5](2eb3cd5))
* **server:** `handleProtocols` accepts arrays too and gracefully rejects other types ([98dec1a](98dec1a)), closes [enisdenjo#318](https://github.com/ilijaNL/graphql-ws/issues/318)
* **server:** `onDisconnect` is called exclusively if the connection is acknowledged ([33ed5f2](33ed5f2))
* **server:** `return` instead of `break` at switch case ends ([e9447e4](e9447e4)), closes [enisdenjo#140](https://github.com/ilijaNL/graphql-ws/issues/140)
* **server:** `subscription` operations are distinct on the message ID ([enisdenjo#24](https://github.com/ilijaNL/graphql-ws/issues/24)) ([dfffb05](dfffb05))
* **server:** allow skipping init message wait with zero values ([a7419df](a7419df))
* **server:** Async iterator must implement `return` ([d99982b](d99982b)), closes [enisdenjo#149](https://github.com/ilijaNL/graphql-ws/issues/149)
* **server:** Client can complete/cancel any operation ([0ad1c4c](0ad1c4c))
* **server:** Close socket if `onSubscribe` returns invalid array ([enisdenjo#53](https://github.com/ilijaNL/graphql-ws/issues/53)) ([0464a54](0464a54))
* **server:** Consistently set `rootValue` and `contextValue`, if not overridden ([enisdenjo#49](https://github.com/ilijaNL/graphql-ws/issues/49)) ([7aa3bcd](7aa3bcd))
* **server:** Distribute server error to all clients even if one error listener throws ([enisdenjo#56](https://github.com/ilijaNL/graphql-ws/issues/56)) ([b96dbb9](b96dbb9))
* **server:** Don't surface bad request error details in production ([enisdenjo#55](https://github.com/ilijaNL/graphql-ws/issues/55)) ([70317b2](70317b2))
* **server:** Enforce ID uniqueness across all operations and during the whole subscription life ([enisdenjo#96](https://github.com/ilijaNL/graphql-ws/issues/96)) ([65d1bfa](65d1bfa))
* **server:** Handle upgrade requests with multiple subprotocols and omit `Sec-WebSocket-Protocol` header if none supported ([9bae064](9bae064))
* **server:** Hide internal server error messages from the client in production ([36fe405](36fe405)), closes [enisdenjo#31](https://github.com/ilijaNL/graphql-ws/issues/31)
* **server:** Init context first on connection open ([a80e753](a80e753)), closes [enisdenjo#181](https://github.com/ilijaNL/graphql-ws/issues/181)
* **server:** Limit internal server error close message size ([8479f76](8479f76))
* **server:** Log internal errors to the console ([6ddf0d1](6ddf0d1))
* **server:** Make sure to use `onSubscribe` result exclusively ([51fdb7c](51fdb7c))
* **server:** No need to bind `this` scope ([f76ac73](f76ac73))
* **server:** Operation result can be async generator or iterable ([b1fb883](b1fb883))
* **server:** Receiving more than one `ConnectionInit` message closes the socket immediately ([757c6e9](757c6e9))
* **server:** Respect completed subscriptions even if `subscribe` or `onOperation` didnt resolve yet ([4700154](4700154))
* **server:** Return ping's payload through the response pong ([47730a9](47730a9)), closes [enisdenjo#117](https://github.com/ilijaNL/graphql-ws/issues/117)
* **server:** scoped execution result formatter from `onConnect` ([f91fadb](f91fadb))
* **server:** Should clean up subscription reservations on abrupt errors without relying on connection close ([611c223](611c223))
* **server:** Shouldn't send a complete message if client sent it ([331fe47](331fe47)), closes [enisdenjo#403](https://github.com/ilijaNL/graphql-ws/issues/403)
* **server:** store the intial request in the context ([6927ee0](6927ee0))
* **server:** Use `subscribe` from the config ([6fbd47c](6fbd47c))
* **server:** use subscription specific formatter for queries and mutations too ([5672a04](5672a04))
* Sink's next callback always receives an `ExecutionResult` ([045b402](045b402))
* Stop sending messages after receiving complete ([enisdenjo#65](https://github.com/ilijaNL/graphql-ws/issues/65)) ([3f4f836](3f4f836))
* Support more `graphql` versions ([de69b4e](de69b4e))
* Support more Node versions by not using `globalThis` ([79c2ed2](79c2ed2))
* Use `4406` close code for unsupported subprotocol (`1002` is an internal WebSocket close code) ([df85281](df85281))
* Use `4500` close code for internal server errors (`1011` is an internal WebSocket close code) ([3c0316d](3c0316d))
* Use `ID` type for message id field ([87ebd35](87ebd35))
* **uWebSockets:** Handle premature and abrupt socket closes ([9d3ff52](9d3ff52)), closes [enisdenjo#186](https://github.com/ilijaNL/graphql-ws/issues/186)
* Warn about subscriptions-transport-ws clients and provide migration link ([e080739](e080739)), closes [enisdenjo#339](https://github.com/ilijaNL/graphql-ws/issues/339) [enisdenjo#325](https://github.com/ilijaNL/graphql-ws/issues/325)
* **ws,fastify-websocket:** Send only on ready socket ([8d13c9e](8d13c9e))
* **ws,uWebSockets,@fastify/websocket:** Handle internal errors that are not instances of `Error` ([enisdenjo#442](https://github.com/ilijaNL/graphql-ws/issues/442)) ([9884889](9884889)), closes [enisdenjo#441](https://github.com/ilijaNL/graphql-ws/issues/441)
* **ws:** Handle socket emitted errors ([a22c00f](a22c00f))
* **ws:** Limit server emitted error close message size ([50620df](50620df))
* **ws:** Log server emitted errors to the console ([0826b0a](0826b0a))
* yarn engine is not required ([enisdenjo#34](https://github.com/ilijaNL/graphql-ws/issues/34)) ([89484b8](89484b8))

### Features

* `cjs`, `esm` and `umd` builds with minification and compression for the browser ([enisdenjo#58](https://github.com/ilijaNL/graphql-ws/issues/58)) ([ebb8dfe](ebb8dfe))
* Add `extensions` field to the subscribe message payload ([d86a8e4](d86a8e4))
* Allow null payloads in messages ([enisdenjo#456](https://github.com/ilijaNL/graphql-ws/issues/456)) ([eeb0265](eeb0265)), closes [enisdenjo#455](https://github.com/ilijaNL/graphql-ws/issues/455)
* Bidirectional ping/pong message types ([enisdenjo#201](https://github.com/ilijaNL/graphql-ws/issues/201)) ([1efaf83](1efaf83))
* Centralise expected close codes in `CloseCode` enum ([d10a75c](d10a75c))
* **client:** `connectionParams` can return `undefined` ([a543187](a543187))
* **client:** `connectionParams` may return a promise ([enisdenjo#71](https://github.com/ilijaNL/graphql-ws/issues/71)) ([33f210c](33f210c))
* **client:** `disablePong` option for when implementing a custom pinger ([6510360](6510360)), closes [enisdenjo#117](https://github.com/ilijaNL/graphql-ws/issues/117)
* **client:** `isFatalConnectionProblem` option for deciding if the connect error should be immediately reported or the connection retried ([enisdenjo#126](https://github.com/ilijaNL/graphql-ws/issues/126)) ([8115871](8115871)), closes [enisdenjo#122](https://github.com/ilijaNL/graphql-ws/issues/122)
* **client:** `onNonLazyError` allows you to catch errors reported in non-lazy mode ([cd1e7df](cd1e7df))
* **client:** `url` option accepts a function or a Promise ([enisdenjo#143](https://github.com/ilijaNL/graphql-ws/issues/143)) ([76f522f](76f522f)), closes [enisdenjo#145](https://github.com/ilijaNL/graphql-ws/issues/145) [enisdenjo#146](https://github.com/ilijaNL/graphql-ws/issues/146)
* **client:** Add `connectionAckWaitTimeout` option ([enisdenjo#228](https://github.com/ilijaNL/graphql-ws/issues/228)) ([35ce054](35ce054))
* **client:** Add `opened` event for when a WebSocket opens ([9053224](9053224))
* **client:** Allow keeping the connection alive for some time before lazy closing ([enisdenjo#69](https://github.com/ilijaNL/graphql-ws/issues/69)) ([555c2c3](555c2c3))
* **client:** Deprecate `isFatalConnectionProblem` option in favour of `shouldRetry` ([d8dcf21](d8dcf21))
* **client:** Emit events for `connecting`, `connected` and `closed` ([627775b](627775b))
* **client:** Implement silent-reconnects ([c6f7872](c6f7872)), closes [enisdenjo#7](https://github.com/ilijaNL/graphql-ws/issues/7)
* **client:** introduce Socky 🧦 - the nifty internal socket state manager ([enisdenjo#8](https://github.com/ilijaNL/graphql-ws/issues/8)) ([a4bee6f](a4bee6f))
* **client:** Lazy option can be changed ([fb0ec14](fb0ec14))
* **client:** Optional `generateID` to provide subscription IDs ([enisdenjo#22](https://github.com/ilijaNL/graphql-ws/issues/22)) ([9a3f54a](9a3f54a)), closes [enisdenjo#21](https://github.com/ilijaNL/graphql-ws/issues/21)
* **client:** Provide subscribe payload in `generateID` ([d0bc6e1](d0bc6e1)), closes [enisdenjo#398](https://github.com/ilijaNL/graphql-ws/issues/398)
* **client:** Re-implement following the new transport protocol ([#6](#6)) ([5191a35](5191a35))
* **client:** Rename `keepAlive` option to `lazyCloseTimeout` ([3c1f13c](3c1f13c))
* **client:** Retry with randomised exponential backoff or provide your own strategy ([enisdenjo#84](https://github.com/ilijaNL/graphql-ws/issues/84)) ([d3e7a17](d3e7a17))
* **client:** Support providing custom WebSocket implementations ([enisdenjo#18](https://github.com/ilijaNL/graphql-ws/issues/18)) ([1515fe2](1515fe2))
* **client:** Terminate the WebSocket abruptly and immediately ([53ad515](53ad515)), closes [enisdenjo#290](https://github.com/ilijaNL/graphql-ws/issues/290)
* Descriptive invalid message errors ([b46379e](b46379e)), closes [enisdenjo#366](https://github.com/ilijaNL/graphql-ws/issues/366)
* Optional `payload` for ping/pong message types ([2fe0345](2fe0345)), closes [enisdenjo#117](https://github.com/ilijaNL/graphql-ws/issues/117)
* Package ECMAScript Modules too ([enisdenjo#87](https://github.com/ilijaNL/graphql-ws/issues/87)) ([2108174](2108174))
* Package rename `@enisdenjo/graphql-transport-ws` 👉 `graphql-transport-ws`. ([494f676](494f676))
* Rewrite GraphQL over WebSocket Protocol ([#2](#2)) ([42045c5](42045c5))
* Send optional payload with the `ConnectionAck` message ([enisdenjo#60](https://github.com/ilijaNL/graphql-ws/issues/60)) ([1327e77](1327e77))
* **server:** `context` may return a promise ([cd5c2f8](cd5c2f8)), closes [enisdenjo#74](https://github.com/ilijaNL/graphql-ws/issues/74)
* **server:** `execute` and `subscribe` are optional ([enisdenjo#148](https://github.com/ilijaNL/graphql-ws/issues/148)) ([af748b0](af748b0))
* **server:** Add `onClose` callback for closures at _any_ point in time ([dd0d4fa](dd0d4fa))
* **server:** Add `onDisconnect` callback ([enisdenjo#94](https://github.com/ilijaNL/graphql-ws/issues/94)) ([2a61268](2a61268))
* **server:** Add support for `ws@v8` ([9119153](9119153))
* **server:** Define execution/subscription `context` in creation options ([5b3d253](5b3d253)), closes [enisdenjo#13](https://github.com/ilijaNL/graphql-ws/issues/13)
* **server:** Dynamic `schema` support by accepting a function or a Promise ([enisdenjo#147](https://github.com/ilijaNL/graphql-ws/issues/147)) ([6a0bf94](6a0bf94)), closes [enisdenjo#127](https://github.com/ilijaNL/graphql-ws/issues/127)
* **server:** For dynamic usage, `context` option can be a function too ([enisdenjo#46](https://github.com/ilijaNL/graphql-ws/issues/46)) ([149b582](149b582))
* **server:** Implement following the new transport protocol ([#1](#1)) ([a412d25](a412d25))
* **server:** Log a warning for unsupported subprotocols ([88a12ef](88a12ef)), closes [enisdenjo#92](https://github.com/ilijaNL/graphql-ws/issues/92)
* **server:** Make and use with your own flavour ([enisdenjo#64](https://github.com/ilijaNL/graphql-ws/issues/64)) ([38bde87](38bde87)), closes [enisdenjo#61](https://github.com/ilijaNL/graphql-ws/issues/61) [enisdenjo#73](https://github.com/ilijaNL/graphql-ws/issues/73) [enisdenjo#75](https://github.com/ilijaNL/graphql-ws/issues/75)
* **server:** More callbacks, clearer differences and higher extensibility ([enisdenjo#40](https://github.com/ilijaNL/graphql-ws/issues/40)) ([507a222](507a222))
* **server:** Optional `onPing` and `onPong` message type listeners ([f36066f](f36066f))
* **server:** Pass roots for operation fields as an option ([dcb5ed4](dcb5ed4))
* **server:** Support returning multiple results from `execute` ([enisdenjo#28](https://github.com/ilijaNL/graphql-ws/issues/28)) ([dbbd88b](dbbd88b))
* **server:** Use `@fastify/websocket` ([enisdenjo#382](https://github.com/ilijaNL/graphql-ws/issues/382)) ([dd755b0](dd755b0)), closes [enisdenjo#381](https://github.com/ilijaNL/graphql-ws/issues/381)
* **server:** Use `fastify-websocket` ([enisdenjo#200](https://github.com/ilijaNL/graphql-ws/issues/200)) ([b62fc95](b62fc95))
* **server:** Use `validate` option for custom GraphQL validation ([b68d56c](b68d56c))
* **server:** Use uWebSockets ([enisdenjo#89](https://github.com/ilijaNL/graphql-ws/issues/89)) ([45d08fc](45d08fc)), closes [enisdenjo#61](https://github.com/ilijaNL/graphql-ws/issues/61)
* Subscribe message `query` must be a string ([enisdenjo#45](https://github.com/ilijaNL/graphql-ws/issues/45)) ([60d9cd5](60d9cd5))
* Support custom JSON message `reviver` and `replacer` ([enisdenjo#172](https://github.com/ilijaNL/graphql-ws/issues/172)) ([0a9894e](0a9894e))
* TypeScript generic for connection init payload (`connectionParams`) ([enisdenjo#311](https://github.com/ilijaNL/graphql-ws/issues/311)) ([e67cf80](e67cf80))
* **use:** Generic for extending the context extras ([401cd4c](401cd4c)), closes [enisdenjo#189](https://github.com/ilijaNL/graphql-ws/issues/189)
* **uWebSockets:** Add `persistedRequest` to context extra and deprecate uWS's stack allocated `request` ([enisdenjo#196](https://github.com/ilijaNL/graphql-ws/issues/196)) ([736e6ed](736e6ed))
* **uWebSockets:** Drop deprecated `request` context extra ([02ea5ee](02ea5ee))
* WebSocket Ping and Pong as keep-alive ([enisdenjo#11](https://github.com/ilijaNL/graphql-ws/issues/11)) ([16ae316](16ae316))

### Performance Improvements

* **client:** Await timeouts only in recursive connects ([55c8fc8](55c8fc8))
* **client:** Focus subscription message listeners on `id` ([enisdenjo#150](https://github.com/ilijaNL/graphql-ws/issues/150)) ([32c2268](32c2268))
* **client:** Memoize message parsing for each subscriber ([2a7ba46](2a7ba46))
* Easier message parser ([d44c6f1](d44c6f1))
* Reduce runtime prototype traversal for hasOwnProperty ([enisdenjo#52](https://github.com/ilijaNL/graphql-ws/issues/52)) ([1bb9218](1bb9218))

### Reverts

* Revert "refactor: emit client connect in next tick during testing" ([c10d0bf](c10d0bf))

### BREAKING CHANGES

* Because of the Protocol's strictness, an instant connection termination will happen whenever an invalid message is identified; meaning, all previous implementations will fail when receiving the new subprotocol ping/pong messages.

**Beware,** the client will NOT ping the server by default. Please make sure to upgrade your stack in order to support the new ping/pong message types.

A simple recipe showcasing a client that times out if no pong is received and measures latency, looks like this:
```js
import { createClient } from 'graphql-ws';

let activeSocket,
  timedOut,
  pingSentAt = 0,
  latency = 0;
createClient({
  url: 'ws://i.time.out:4000/and-measure/latency',
  keepAlive: 10_000, // ping server every 10 seconds
  on: {
    connected: (socket) => (activeSocket = socket),
    ping: (received) => {
      if (!received /* sent */) {
        pingSentAt = Date.now();
        timedOut = setTimeout(() => {
          if (activeSocket.readyState === WebSocket.OPEN)
            activeSocket.close(4408, 'Request Timeout');
        }, 5_000); // wait 5 seconds for the pong and then close the connection
      }
    },
    pong: (received) => {
      if (received) {
        latency = Date.now() - pingSentAt;
        clearTimeout(timedOut); // pong is received, clear connection close timeout
      }
    },
  },
});
```
* **uWebSockets:** The deprecated uWebSockets `request` context extra field has been dropped because it is stack allocated and cannot be used ouside the internal `upgrade` callback.
* **client:** Client `keepAlive` option has been renamed to `lazyCloseTimeout` in order to eliminate ambiguity with the client to server pings keep-alive option.
* **server:** The return function of `server.opened` (`closed`) now requires the close event code and reason for reporting to the `onDisconnect` callback.
* **server:** The `Context.subscriptions` record value can be either an `AsyncIterator` or a `Promise`.
* **client:** Client `retryTimeout` option has been replaced with the new `retryWait`.

`retryWait` allows you to control the retry timeout strategy by resolving the returned promise when ready. The default implements the randomised exponential backoff like so:
```ts
// this is the default
const retryWait = async function randomisedExponentialBackoff(retries: number) {
  let retryDelay = 1000; // start with 1s delay
  for (let i = 0; i < retries; i++) {
    retryDelay *= 2; // square `retries` times
  }
  await new Promise((resolve) =>
    setTimeout(
      // resolve pending promise with added random timeout from 300ms to 3s
      resolve,
      retryDelay + Math.floor(Math.random() * (3000 - 300) + 300),
    ),
  );
};
```
* **server:** You now "make" a ready-to-use server that can be used with _any_ WebSocket implementation!

Summary of breaking changes:
- No more `keepAlive`. The user should provide its own keep-alive implementation. _(I highly recommend [WebSocket Ping and Pongs](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Pings_and_Pongs_The_Heartbeat_of_WebSockets))_
- No more HTTP `request` in the server context.
- No more WebSocket in the server context (you're the one that creates it).
- You use your own WebSocket server
- Server exports only `makeServer` _(no more `createServer`)_

### Benefits
- You're responsible for the server (_any_ optimisation or adjustment can be applied)
- Any WebSocket server can be used (or even mocked if necessary)
- You control the disposal of the server (close or transfer clients however you wish)
- New `extra` field in the `Context` for storing custom values useful for callbacks
- Full control of authentication flow
- Full control over error handling
- True zero-dependency

### Migrating from v1

**Only the server has to be migrated.** Since this release allows you to use your favourite WebSocket library (or your own implementation), using [ws](https://github.com/websockets/ws) is just one way of using `graphql-ws`. This is how to use the implementation shipped with the lib:

```ts
/**
 * ❌ instead of the lib creating a WebSocket server internally with the provided arguments
 */
import https from 'https';
import { createServer } from 'graphql-ws';

const server = https.createServer(...);

createServer(
  {
    onConnect(ctx) {
      // were previously directly on the context
      ctx.request as IncomingRequest
      ctx.socket as WebSocket
    },
    ...rest,
  },
  {
    server,
    path: '/graphql',
  },
);

/**
 * ✅ you have to supply the server yourself
 */
import https from 'https';
import ws from 'ws'; // yarn add ws
import { useServer } from 'graphql-ws/lib/use/ws'; // notice the import path

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

useServer(
  {
    onConnect(ctx) {
      // are now in the `extra` field
      ctx.extra.request as IncomingRequest
      ctx.extra.socket as WebSocket
    },
    ...rest,
  },
  wsServer,
  // optional keepAlive with ping pongs (defaults to 12 seconds)
);
```
* This lib is no longer compatible with [`subscriptions-transport-ws`](https://github.com/apollographql/subscriptions-transport-ws). It follows a redesigned transport protocol aiming to improve security, stability and reduce ambiguity.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working released Has been released and published
Projects
None yet
Development

No branches or pull requests

2 participants