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

Breaking changes #1904

Merged
merged 9 commits into from
Jul 14, 2021
Merged

Breaking changes #1904

merged 9 commits into from
Jul 14, 2021

Conversation

lpinca
Copy link
Member

@lpinca lpinca commented Jun 18, 2021

I'm opening this PR to get a feedback on some breaking changes that I have been delaying for a while and planning to release in a new major version soon.

@lpinca lpinca force-pushed the breaking-changes branch 5 times, most recently from ae5362b to fba0658 Compare June 23, 2021 09:57
@lpinca
Copy link
Member Author

lpinca commented Jul 2, 2021

I'm not sure if ab7b6d4 makes sense. It is good for performance but adapting existing code to this change might require a little effort. Users have call buffer.toString() themselves:

ws.on('message', function (data, isBinary) {
  const message = isBinary ? data : data.toString();
  // Continue as before.
});

ws.on('close', function (code, data) {
  const reason = data.toString();
  // continue as before.
});

If anyone is watching this, please share your opinions.

@jimmywarting
Copy link

jimmywarting commented Jul 5, 2021

I'm not sure if ab7b6d4 makes sense. It is good for performance but adapting existing code to this change might require a little effort. Users have call buffer.toString() themselves

I say: emit a ArrayBuffer (or Blob) & follow the spec instead do not emit Buffer or strings, do what is best for cross platform coding, ppl should build there application so they work in Deno or browser the same way


if you are planing on making a breaking change anyway, what do you feel about switching to ESM? node "^12.20.0 || ^14.13.0"

While we are at it, go for Event and EventTarget that exist in NodeJS now

Fyi, Node ships with experimental Blob support now now (v15.7) also, If you switch to ESM then you can use fetch-blob as a polyfill/fallback it only exports ESM (it ditched commonjs)

@lpinca
Copy link
Member Author

lpinca commented Jul 5, 2021

@jimmywarting ab7b6d4 is about using Buffers instead of strings for text messages and for the close reason, not for binary messages. This is against the HTML specification where a DOMString is used and it is done for performance. For binary messages, users can already get an ArrayBuffer by setting the binaryType attribute to 'arraybuffer' as per HTML spec.

If you are planing on making a breaking change anyway, what do you feel about switching to ESM? node "^12.20.0 || ^14.13.0"

See #1886. I'm fine with that as long as it does not cause issues with bundlers.

While we are at it, go for Event and EventTarget that exist in NodeJS now

This requires Node.js >= 14.5.0 and comes with significant performance regressions. This is currently a no go.

Fyi, Node ships with experimental Blob support now now (v15.7) also

I know and it might make sense to change the default binaryType from 'nodebuffer' to 'blob' as per HTML spec but the feature is experimental and I don't want to use polyfills. I prefer to postpone this to the next major release.

@lpinca lpinca force-pushed the breaking-changes branch 5 times, most recently from b9eb026 to b60712d Compare July 6, 2021 08:10
@lpinca
Copy link
Member Author

lpinca commented Jul 6, 2021

I really need feedbacks for 4edae51. If it is too breaking I will remove it.

cc: @cTn-dev @pimterry @netizen-ais @feross @mcollina

@cTn-dev
Copy link
Contributor

cTn-dev commented Jul 6, 2021

This is definitely one hell of a breaking change.

While i am in favor of removing the "implicit" .toString() and let the user handle that logic (as already pointed out in some cases, it would be pretty invisible as far JSON.parse() is concerned.

Have the removal of the .toString() resulted in any significant performance improvement ?
I don't currently have an opinion on the isBinary argument (will have to think about it some more)

@lpinca
Copy link
Member Author

lpinca commented Jul 6, 2021

Have the removal of the .toString() resulted in any significant performance improvement ?

Yes, especially when using the streaming interface. It prevents the data from being copied twice (from Buffer to string when a text message is received and back from string to Buffer when it is added to the internal queue of the Duplex) here https://github.com/websockets/ws/blob/7.5.2/lib/stream.js#L76.

Another similar situation is when sending a text message from a client to another:

wss.on('connection', function (ws) {
  ws.on('message', function (data, isBinary) {
    ws.send(data, { binary: isBinary});
  });
});

data is not copied twice in this case.

@netizen-ais
Copy link

My environment isn't big enough to get a hint about performance (2 ws servers with an average of 100 clients that connect to both node processes), but after testing this branch in the develop environment with no code changes on our base, I've tested it in one of the ws servers without any problem.

Copy link
Contributor

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with the changes!

"browser": "browser.js",
"engines": {
"node": ">=8.3.0"
"node": ">=10.0.0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend to make the jump to v12.

Copy link
Member Author

@lpinca lpinca Jul 6, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v10 is officially no longer supported but only two months have passed since its EOL date. I prefer to wait a little longer before dropping support for it, there is no additional maintenance burden compared to v12.

@pimterry
Copy link
Contributor

pimterry commented Jul 6, 2021

Changes all make sense to me, definitely required quite a few updates on my end so it is a bit disruptive, but all very small and easy to do, and everything's still working nicely afterwards 👍

@cTn-dev
Copy link
Contributor

cTn-dev commented Jul 8, 2021

I did a bit of testing against the 10f27d7 with both string and binary messages (haven't tried a mixing them in a single session).
Everything is working fine, the "migration" due to the breaking changes took < 5 minutes, as straight forward as it gets.

Make the `WebSocket` constructor throw a `SyntaxError` if any of the
subprotocol names are invalid or duplicated.
Abort the handshake if the `Sec-WebSocket-Protocol` header is invalid.
Make the parser throw an error if the header field value is empty or if
it begins or ends with a white space.
Make the `WebSocket` constructor throw a `SyntaxError` if the URL
contains a fragment identifier or if the URL's protocol is not one of
`'ws:'`, `'wss:'`, or `'ws+unix:'`.
Avoid decoding text messages and close reasons to strings. Pass them as
`Buffer`s to the listeners of their respective events. Also, make
listeners of the `'message'` event take a boolean argument to speficy
whether or not the message is binary.

Refs: #1878
Refs: #1804
@jberger
Copy link

jberger commented Jul 12, 2021

I've just hit something that is obliquely related. I can open a new bug (and if it gets ignored here I will) but currently it seems to be impossible to write to a websocket stream as text. Any idea if you can make the writeableObjectMode useful while you make the readableObjectMode useful too?

(This affects me because the code that controls the websocket in the browser side assumes that it gets text and I have no control over that code https://github.com/glyptodon/guacamole-client/blob/754e9649f1fa0ba225ee42b56ded64bc283d17df/guacamole-common-js/src/main/webapp/modules/Tunnel.js#L1009)

It seems that a minimal fix is to simply allow the options to override the defaults that you give it https://github.com/websockets/ws/blob/master/lib/stream.js#L68 . I know that leaves the client open to breaking other options but hopefully "you break it, you bought it" applies.

@lpinca
Copy link
Member Author

lpinca commented Jul 13, 2021

I can open a new bug (and if it gets ignored here I will) but currently it seems to be impossible to write to a websocket stream as text.

@jberger this is unrelated to this PR so yes, a new issue would have been better. Anyway you can use the decodeStrings option.

const WebSocket = require('ws');

const ws = new WebSocket(url);
const duplex = WebSocket.createWebSocketStream(ws, { decodeStrings: false });

duplex.write('foo');
duplex.write('bar');
duplex.write('baz');

@lpinca
Copy link
Member Author

lpinca commented Jul 13, 2021

@pimterry the last two commits are based on our discussion in #1902. Can you please take a look when you have some time? Thank you.

@lpinca lpinca force-pushed the breaking-changes branch 2 times, most recently from e98f3be to 09334ff Compare July 13, 2021 07:38
doc/ws.md Outdated
@@ -209,7 +209,8 @@ added when the `clientTracking` is truthy.

Close the HTTP server if created internally, terminate all clients and call
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably say 'close' all clients now, not 'terminate'.

if (!this.clients.size) {
process.nextTick(emitClose, this);
} else {
for (const client of this.clients) client.close(1001);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a networking POV, this all seems like a good improvement to me. It definitely seems clearer for clients if servers send a 1001 close frame and tidily shutdown connections where possible 👍

That said, it does mean the connections could now unexpectedly stay open whilst waiting for a close response, until the close timeout, if you have any poorly behaving clients. That might be problematic if users are trying to immediately shut everything down, which seems like a reasonable use case, and there's now no easy alternative way to do that when necessary.

Maybe the server should have a server.terminate() method too, which does terminate everything immediately?

That would make the server API match the individual websockets API, which already has websocket.terminate(), and it would cover that immediate-shutdown use case.

As a bonus, this could also easily support cases like "try to shut down cleanly, but then always close within X milliseconds" with just:

server.close();
setTimeout(() => server.terminate(), X);

Copy link
Member Author

@lpinca lpinca Jul 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users can terminate clients by themself if they want to close immediately:

wss.close();
for (const ws of wss.clients) ws.terminate();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, fair enough. I'm not sure if that trick will be obvious to people, so a convenience server.terminate() method to do the same could still be useful, but you're right that that means it's not strictly necessary 👍

It's probably worth mentioning this in the breaking-changes release notes though imo, as the suggested way to migrate for people who do need the previous behaviour.

Copy link
Member Author

@lpinca lpinca Jul 13, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have two doubts:

  1. If it is better to just prevent new connections from being established and not close/terminate existing clients. Closing clients is user responsibility so they can use websocket.close() or websocket.terminate() at will. This is also consistent with the behavior of server.close().
  2. Regardless of 1., if/when to emit the 'close' event when client tracking is disabled. This is currently done in the next tick.

Copy link
Member Author

@lpinca lpinca Jul 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and changed WebSocketServer.prototype.close() behavior in 6a238da. It no longer closes existing connections as per point 1. of my previous comment.

I've also updated the documentation for point 2. I think that emitting the 'close' event when connections are still active is a bit misleading but this behavior already exists and was not changed here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed this sorry, I've been busy. That sounds very reasonable to me and the consistency with node is great.

I do think documenting this clearly for those who do want to terminate the server will be useful though, and it's not obvious right now. It might be worth a "how to fully shutdown a server" section in the readme, and/or the release notes.

@lpinca lpinca force-pushed the breaking-changes branch 2 times, most recently from 2da7977 to c8615c4 Compare July 14, 2021 15:10
When `WebSocketServer.prototype.close()` is called, stop accepting new
connections but do not close the existing ones.

If the HTTP/S server was created internally, then close it and emit the
`'close'` event when it closes. Otherwise, if client tracking is
enabled, then emit the `'close'` event when the number of connections
goes down to zero. Otherwise, emit it in the next tick.

Refs: #1902
Match the behavior of Node.js core `net.Server` and call the callback
of `WebSocketServer.prototype.close()` with an error if the server is
already closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants