Skip to content

Introduce the 'wsClientError' event #2046

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

Merged
merged 1 commit into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions doc/ws.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [Event: 'error'](#event-error)
- [Event: 'headers'](#event-headers)
- [Event: 'listening'](#event-listening)
- [Event: 'wsClientError'](event-wsclienterror)
- [server.address()](#serveraddress)
- [server.clients](#serverclients)
- [server.close([callback])](#serverclosecallback)
Expand Down Expand Up @@ -202,6 +203,21 @@ handshake. This allows you to inspect/modify the headers before they are sent.

Emitted when the underlying server has been bound.

### Event: 'wsClientError'

- `error` {Error}
- `socket` {net.Socket|tls.Socket}
- `request` {http.IncomingMessage}

Emitted when an error occurs before the WebSocket connection is established.
`socket` and `request` are respectively the socket and the HTTP request from
which the error originated. The listener of this event is responsible for
closing the socket. When the `'wsClientError'` event is emitted there is no
`http.ServerResponse` object, so any HTTP response, including the response
headers and body, must be written directly to the `socket`. If there is no
listener for this event, the socket is closed with a default 4xx response
containing a descriptive error message.

### server.address()

Returns an object with `port`, `family`, and `address` properties specifying the
Expand Down
36 changes: 30 additions & 6 deletions lib/websocket-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,24 +234,26 @@ class WebSocketServer extends EventEmitter {
const version = +req.headers['sec-websocket-version'];

if (req.method !== 'GET') {
abortHandshake(socket, 405, 'Invalid HTTP method');
const message = 'Invalid HTTP method';
abortHandshakeOrEmitwsClientError(this, req, socket, 405, message);
return;
}

if (req.headers.upgrade.toLowerCase() !== 'websocket') {
abortHandshake(socket, 400, 'Invalid Upgrade header');
const message = 'Invalid Upgrade header';
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
return;
}

if (!key || !keyRegex.test(key)) {
const message = 'Missing or invalid Sec-WebSocket-Key header';
abortHandshake(socket, 400, message);
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
return;
}

if (version !== 8 && version !== 13) {
const message = 'Missing or invalid Sec-WebSocket-Version header';
abortHandshake(socket, 400, message);
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
return;
}

Expand All @@ -268,7 +270,7 @@ class WebSocketServer extends EventEmitter {
protocols = subprotocol.parse(secWebSocketProtocol);
} catch (err) {
const message = 'Invalid Sec-WebSocket-Protocol header';
abortHandshake(socket, 400, message);
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
return;
}
}
Expand Down Expand Up @@ -296,7 +298,7 @@ class WebSocketServer extends EventEmitter {
} catch (err) {
const message =
'Invalid or unacceptable Sec-WebSocket-Extensions header';
abortHandshake(socket, 400, message);
abortHandshakeOrEmitwsClientError(this, req, socket, 400, message);
return;
}
}
Expand Down Expand Up @@ -509,3 +511,25 @@ function abortHandshake(socket, code, message, headers) {
message
);
}

/**
* Emit a `'wsClientError'` event on a `WebSocketServer` if there is at least
* one listener for it, otherwise call `abortHandshake()`.
*
* @param {WebSocketServer} server The WebSocket server
* @param {http.IncomingMessage} req The request object
* @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request
* @param {Number} code The HTTP response status code
* @param {String} message The HTTP response body
* @private
*/
function abortHandshakeOrEmitwsClientError(server, req, socket, code, message) {
if (server.listenerCount('wsClientError')) {
const err = new Error(message);
Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);

server.emit('wsClientError', err, socket, req);
} else {
abortHandshake(socket, code, message);
}
}
34 changes: 34 additions & 0 deletions test/websocket-server.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,40 @@ describe('WebSocketServer', () => {
});
});

it("emits the 'wsClientError' event", (done) => {
const wss = new WebSocket.Server({ port: 0 }, () => {
const req = http.request({
method: 'POST',
port: wss.address().port,
headers: {
Connection: 'Upgrade',
Upgrade: 'websocket'
}
});

req.on('response', (res) => {
assert.strictEqual(res.statusCode, 400);
wss.close(done);
});

req.end();
});

wss.on('wsClientError', (err, socket, request) => {
assert.ok(err instanceof Error);
assert.strictEqual(err.message, 'Invalid HTTP method');

assert.ok(request instanceof http.IncomingMessage);
assert.strictEqual(request.method, 'POST');

socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});

wss.on('connection', () => {
done(new Error("Unexpected 'connection' event"));
});
});

it('fails if the WebSocket server is closing or closed', (done) => {
const server = http.createServer();
const wss = new WebSocket.Server({ noServer: true });
Expand Down