From f8dd60d2bfa18cb6050c6e9c3250a8ed2f433db9 Mon Sep 17 00:00:00 2001 From: Gadzhi Gadzhiev <168296+resure@users.noreply.github.com> Date: Wed, 30 Sep 2026 22:59:17 +0300 Subject: [PATCH] fix: resolve the WebSocket URL from the page origin in blob workers --- client-src/index.ts | 23 +++++--- tests/createSocketURL.test.ts | 98 +++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 tests/createSocketURL.test.ts diff --git a/client-src/index.ts b/client-src/index.ts index 9ba73d5..8df68c0 100644 --- a/client-src/index.ts +++ b/client-src/index.ts @@ -585,6 +585,13 @@ const formatURL = (objURL: { const createSocketURL = ( parsedURL: URL & { fromCurrentScript?: boolean }, ): string => { + // Blob workers have an empty hostname; their origin is the page that created them. + const pageLocation = + self.location.protocol === 'blob:' && + self.location.origin && + self.location.origin !== 'null' + ? new URL(self.location.origin) + : self.location; let { hostname } = parsedURL; // Node.js module parses it as `::` @@ -597,20 +604,20 @@ const createSocketURL = ( // see: https://github.com/webpack/webpack-dev-server/pull/384 if ( isInAddrAny && - self.location.hostname && - self.location.protocol.indexOf('http') === 0 + pageLocation.hostname && + pageLocation.protocol.indexOf('http') === 0 ) { - hostname = self.location.hostname; + hostname = pageLocation.hostname; } - let socketURLProtocol = parsedURL.protocol || self.location.protocol; + let socketURLProtocol = parsedURL.protocol || pageLocation.protocol; // When https is used in the app, secure web sockets are always necessary because the browser doesn't accept non-secure web sockets. if ( socketURLProtocol === 'auto:' || - (hostname && isInAddrAny && self.location.protocol === 'https:') + (hostname && isInAddrAny && pageLocation.protocol === 'https:') ) { - socketURLProtocol = self.location.protocol; + socketURLProtocol = pageLocation.protocol; } socketURLProtocol = socketURLProtocol.replace( @@ -643,14 +650,14 @@ const createSocketURL = ( // so we need to fall back to the default if they are not provided const socketURLHostname = ( hostname || - self.location.hostname || + pageLocation.hostname || 'localhost' ).replace(/^\[(.*)\]$/, '$1'); let socketURLPort = parsedURL.port; if (!socketURLPort || socketURLPort === '0') { - socketURLPort = self.location.port; + socketURLPort = pageLocation.port; } // If path is provided it'll be passed in via the resourceQuery as a diff --git a/tests/createSocketURL.test.ts b/tests/createSocketURL.test.ts new file mode 100644 index 0000000..0063c82 --- /dev/null +++ b/tests/createSocketURL.test.ts @@ -0,0 +1,98 @@ +import { rstest } from 'rstack/test'; + +rstest.mock('../client-src/socket.js', () => ({ default: rstest.fn() })); + +let createSocketURL: typeof import('../client-src/index').createSocketURL; + +beforeAll(async () => { + rstest.stubGlobal('BUILD_HASH', 'test'); + rstest.stubGlobal('RESOURCE_QUERY', '?hostname=0.0.0.0&protocol=ws:&port=0'); + rstest.stubGlobal('self', { + location: new URL('http://localhost/'), + addEventListener: rstest.fn(), + }); + ({ createSocketURL } = await import('../client-src/index')); +}); + +afterAll(() => rstest.unstubAllGlobals()); + +describe('createSocketURL', () => { + it.each([ + ['https://example.com', 'ws:', '0.0.0.0', '0', 'wss://example.com/ws'], + ['http://localhost:8080', 'ws:', '0.0.0.0', '0', 'ws://localhost:8080/ws'], + [ + 'https://example.com:8443', + 'ws:', + '0.0.0.0', + '0', + 'wss://example.com:8443/ws', + ], + ['https://example.com', 'auto:', '0.0.0.0', '', 'wss://example.com/ws'], + ['https://example.com', '', '', '', 'wss://example.com/ws'], + ['https://example.com', 'ws:', '::', '0', 'wss://example.com/ws'], + ['https://example.com', 'ws:', '[::]', '0', 'wss://example.com/ws'], + [ + 'https://example.com', + 'ws:', + 'dev.example.com', + '9000', + 'ws://dev.example.com:9000/ws', + ], + [ + 'http://localhost:8080', + 'wss:', + 'dev.example.com', + '443', + 'wss://dev.example.com:443/ws', + ], + [ + 'http://localhost:8080', + 'wss:', + '0.0.0.0', + '9000', + 'wss://localhost:9000/ws', + ], + ])( + 'uses blob origin %s with protocol=%s, hostname=%s, port=%s', + (origin, protocol, hostname, port, expected) => { + const location = new URL(`blob:${origin}/worker-id`); + expect(location.hostname).toBe(''); + rstest.stubGlobal('self', { location }); + expect(createSocketURL({ protocol, hostname, port } as URL)).toBe( + expected, + ); + }, + ); + + it('preserves a configured path and credentials', () => { + rstest.stubGlobal('self', { + location: new URL('blob:https://example.com/worker-id'), + }); + expect( + createSocketURL({ + protocol: 'ws:', + hostname: '0.0.0.0', + port: '0', + pathname: '/build/sockjs-node', + username: 'user', + password: 'password', + } as URL), + ).toBe('wss://user:password@example.com/build/sockjs-node'); + }); + + it.each([ + ['https://example.com:8443/page', 'wss://example.com:8443/ws'], + ['http://localhost:8080/worker.js', 'ws://localhost:8080/ws'], + ['blob:null/worker-id', 'ws://0.0.0.0/ws'], + ['data:text/javascript,', 'ws://0.0.0.0/ws'], + ])('preserves existing behavior for %s', (href, expected) => { + rstest.stubGlobal('self', { location: new URL(href) }); + expect( + createSocketURL({ + protocol: 'ws:', + hostname: '0.0.0.0', + port: '0', + } as URL), + ).toBe(expected); + }); +});