Repository: websockets/ws
Pull request #2162 (2023-08-20 15:49:28)

Allow http and https schemes
Allow HTTP(S) URLs to be used in the WebSocket constructor. They are immediately converted to the ws and wss schemes.

Refs: https://github.com/whatwg/websockets/pull/45


Patch:
diff --git a/lib/receiver.js b/lib/receiver.js
index 96f572cb..b5e9a8bc 100644
--- a/lib/receiver.js
+++ b/lib/receiver.js
@@ -13,12 +13,15 @@ const { concat, toArrayBuffer, unmask } = require('./buffer-util');
 const { isValidStatusCode, isValidUTF8 } = require('./validation');
 
 const FastBuffer = Buffer[Symbol.species];
+const promise = Promise.resolve();
+
 const GET_INFO = 0;
 const GET_PAYLOAD_LENGTH_16 = 1;
 const GET_PAYLOAD_LENGTH_64 = 2;
 const GET_MASK = 3;
 const GET_DATA = 4;
 const INFLATING = 5;
+const WAIT_MICROTASK = 6;
 
 /**
  * HyBi Receiver implementation.
@@ -157,9 +160,23 @@ class Receiver extends Writable {
         case GET_DATA:
           err = this.getData(cb);
           break;
+        case INFLATING:
+          this._loop = false;
+          return;
         default:
-          // `INFLATING`
+          //
+          // `WAIT_MICROTASK`.
+          //
           this._loop = false;
+
+          //
+          // `queueMicrotask()` is not available in Node.js < 11 and is no
+          // better anyway.
+          //
+          promise.then(() => {
+            this._state = GET_INFO;
+            this.startLoop(cb);
+          });
           return;
       }
     } while (this._loop);
@@ -542,7 +559,7 @@ class Receiver extends Writable {
       }
     }
 
-    this._state = GET_INFO;
+    this._state = WAIT_MICROTASK;
   }
 
   /**
@@ -559,6 +576,8 @@ class Receiver extends Writable {
       if (data.length === 0) {
         this.emit('conclude', 1005, EMPTY_BUFFER);
         this.end();
+
+        this._state = GET_INFO;
       } else {
         const code = data.readUInt16BE(0);
 
@@ -590,14 +609,16 @@ class Receiver extends Writable {
 
         this.emit('conclude', code, buf);
         this.end();
+
+        this._state = GET_INFO;
       }
     } else if (this._opcode === 0x09) {
       this.emit('ping', data);
+      this._state = WAIT_MICROTASK;
     } else {
       this.emit('pong', data);
+      this._state = WAIT_MICROTASK;
     }
-
-    this._state = GET_INFO;
   }
 }
 
diff --git a/lib/websocket.js b/lib/websocket.js
index b2b2b092..f71d3d8e 100644
--- a/lib/websocket.js
+++ b/lib/websocket.js
@@ -667,24 +667,30 @@ function initAsClient(websocket, address, protocols, options) {
 
   if (address instanceof URL) {
     parsedUrl = address;
-    websocket._url = address.href;
   } else {
     try {
       parsedUrl = new URL(address);
     } catch (e) {
       throw new SyntaxError(`Invalid URL: ${address}`);
     }
+  }
 
-    websocket._url = address;
+  if (parsedUrl.protocol === 'http:') {
+    parsedUrl.protocol = 'ws:';
+  } else if (parsedUrl.protocol === 'https:') {
+    parsedUrl.protocol = 'wss:';
   }
 
+  websocket._url = parsedUrl.href;
+
   const isSecure = parsedUrl.protocol === 'wss:';
   const isIpcUrl = parsedUrl.protocol === 'ws+unix:';
   let invalidUrlMessage;
 
   if (parsedUrl.protocol !== 'ws:' && !isSecure && !isIpcUrl) {
     invalidUrlMessage =
-      'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"';
+      'The URL\'s protocol must be one of "ws:", "wss:", ' +
+      '"http:", "https", or "ws+unix:"';
   } else if (isIpcUrl && !parsedUrl.pathname) {
     invalidUrlMessage = "The URL's pathname is empty";
   } else if (parsedUrl.hash) {


Test patch:
diff --git a/test/receiver.test.js b/test/receiver.test.js
index 4ae27946..a4e1bb5a 100644
--- a/test/receiver.test.js
+++ b/test/receiver.test.js
@@ -1083,4 +1083,96 @@ describe('Receiver', () => {
 
     receiver.write(Buffer.from([0x88, 0x03, 0x03, 0xe8, 0xf8]));
   });
+
+  it("waits a microtask after each 'message' event", (done) => {
+    const messages = [];
+    const receiver = new Receiver();
+
+    receiver.on('message', (data, isBinary) => {
+      assert.ok(!isBinary);
+
+      const message = data.toString();
+      messages.push(message);
+
+      // `queueMicrotask()` is not available in Node.js < 11.
+      Promise.resolve().then(() => {
+        messages.push(`microtask ${message}`);
+
+        if (messages.length === 6) {
+          assert.deepStrictEqual(messages, [
+            '1',
+            'microtask 1',
+            '2',
+            'microtask 2',
+            '3',
+            'microtask 3'
+          ]);
+
+          done();
+        }
+      });
+    });
+
+    receiver.write(Buffer.from('810131810132810133', 'hex'));
+  });
+
+  it("waits a microtask after each 'ping' event", (done) => {
+    const actual = [];
+    const receiver = new Receiver();
+
+    receiver.on('ping', (data) => {
+      const message = data.toString();
+      actual.push(message);
+
+      // `queueMicrotask()` is not available in Node.js < 11.
+      Promise.resolve().then(() => {
+        actual.push(`microtask ${message}`);
+
+        if (actual.length === 6) {
+          assert.deepStrictEqual(actual, [
+            '1',
+            'microtask 1',
+            '2',
+            'microtask 2',
+            '3',
+            'microtask 3'
+          ]);
+
+          done();
+        }
+      });
+    });
+
+    receiver.write(Buffer.from('890131890132890133', 'hex'));
+  });
+
+  it("waits a microtask after each 'pong' event", (done) => {
+    const actual = [];
+    const receiver = new Receiver();
+
+    receiver.on('pong', (data) => {
+      const message = data.toString();
+      actual.push(message);
+
+      // `queueMicrotask()` is not available in Node.js < 11.
+      Promise.resolve().then(() => {
+        actual.push(`microtask ${message}`);
+
+        if (actual.length === 6) {
+          assert.deepStrictEqual(actual, [
+            '1',
+            'microtask 1',
+            '2',
+            'microtask 2',
+            '3',
+            'microtask 3'
+          ]);
+
+          done();
+        }
+      });
+    });
+
+    receiver.write(Buffer.from('8A01318A01328A0133', 'hex'));
+  });
 });
diff --git a/test/websocket.test.js b/test/websocket.test.js
index 7b397842..24553a1f 100644
--- a/test/websocket.test.js
+++ b/test/websocket.test.js
@@ -36,8 +36,16 @@ describe('WebSocket', () => {
       );
 
       assert.throws(
-        () => new WebSocket('https://websocket-echo.com'),
-        /^SyntaxError: The URL's protocol must be one of "ws:", "wss:", or "ws\+unix:"$/
+        () => new WebSocket('bad-scheme://websocket-echo.com'),
+        (err) => {
+          assert.strictEqual(
+            err.message,
+            'The URL\'s protocol must be one of "ws:", "wss:", ' +
+              '"http:", "https", or "ws+unix:"'
+          );
+
+          return true;
+        }
       );
 
       assert.throws(
@@ -72,6 +80,30 @@ describe('WebSocket', () => {
       const ws = new WebSocket(new URL('ws://[::1]'), { agent });
     });
 
+    it('allows the http scheme', (done) => {
+      const agent = new CustomAgent();
+
+      agent.addRequest = (req, opts) => {
+        assert.strictEqual(opts.host, 'localhost');
+        assert.strictEqual(opts.port, 80);
+        done();
+      };
+
+      const ws = new WebSocket('http://localhost', { agent });
+    });
+
+    it('allows the https scheme', (done) => {
+      const agent = new https.Agent();
+
+      agent.addRequest = (req, opts) => {
+        assert.strictEqual(opts.host, 'localhost');
+        assert.strictEqual(opts.port, 443);
+        done();
+      };
+
+      const ws = new WebSocket('https://localhost', { agent });
+    });
+
     describe('options', () => {
       it('accepts the `options` object as 3rd argument', () => {
         const agent = new http.Agent();
@@ -539,10 +571,18 @@ describe('WebSocket', () => {
       });
 
       it('exposes the server url', () => {
-        const url = 'ws://localhost';
-        const ws = new WebSocket(url, { agent: new CustomAgent() });
+        const schemes = new Map([
+          ['ws', 'ws'],
+          ['wss', 'wss'],
+          ['http', 'ws'],
+          ['https', 'wss']
+        ]);
 
-        assert.strictEqual(ws.url, url);
+        for (const [key, value] of schemes) {
+          const ws = new WebSocket(`${key}://localhost/`, { lookup() {} });
+
+          assert.strictEqual(ws.url, `${value}://localhost/`);
+        }
       });
     });
   });
@@ -1174,7 +1214,9 @@ describe('WebSocket', () => {
 
     it('emits an error if the redirect URL is invalid (2/2)', (done) => {
       server.once('upgrade', (req, socket) => {
-        socket.end('HTTP/1.1 302 Found\r\nLocation: http://localhost\r\n\r\n');
+        socket.end(
+          'HTTP/1.1 302 Found\r\nLocation: bad-scheme://localhost\r\n\r\n'
+        );
       });
 
       const ws = new WebSocket(`ws://localhost:${server.address().port}`, {
@@ -1186,7 +1228,8 @@ describe('WebSocket', () => {
         assert.ok(err instanceof SyntaxError);
         assert.strictEqual(
           err.message,
-          'The URL\'s protocol must be one of "ws:", "wss:", or "ws+unix:"'
+          'The URL\'s protocol must be one of "ws:", "wss:", ' +
+            '"http:", "https", or "ws+unix:"'
         );
         assert.strictEqual(ws._redirects, 1);
 
@@ -2424,7 +2467,7 @@ describe('WebSocket', () => {
 
     it('can send a big binary message', (done) => {
       const wss = new WebSocket.Server({ port: 0 }, () => {
-        const array = new Float32Array(5 * 1024 * 1024);
+        const array = new Float32Array(1024 * 1024);
 
         for (let i = 0; i < array.length; i++) {
           array[i] = i / 5;
@@ -4075,18 +4118,18 @@ describe('WebSocket', () => {
             const messages = [];
             const ws = new WebSocket(`ws://localhost:${wss.address().port}`);
 
-            ws.on('open', () => {
-              ws._socket.on('end', () => {
-                assert.strictEqual(ws._receiver._state, 5);
-              });
-            });
-
             ws.on('message', (message, isBinary) => {
               assert.ok(!isBinary);
 
               if (messages.push(message.toString()) > 1) return;
 
-              ws.close(1000);
+              // `queueMicrotask()` is not available in Node.js < 11.
+              Promise.resolve().then(() => {
+                process.nextTick(() => {
+                  assert.strictEqual(ws._receiver._state, 5);
+                  ws.close(1000);
+                });
+              });
             });
 
             ws.on('close', (code, reason) => {
@@ -4331,9 +4374,12 @@ describe('WebSocket', () => {
 
               if (messages.push(message.toString()) > 1) return;
 
-              process.nextTick(() => {
-                assert.strictEqual(ws._receiver._state, 5);
-                ws.terminate();
+              // `queueMicrotask()` is not available in Node.js < 11.
+              Promise.resolve().then(() => {
+                process.nextTick(() => {
+                  assert.strictEqual(ws._receiver._state, 5);
+                  ws.terminate();
+                });
               });
             });