From bad8be685749e826e70294105e25e62c75f9610f Mon Sep 17 00:00:00 2001 From: Siddharth Gelera Date: Tue, 30 Sep 2025 18:45:24 +0530 Subject: [PATCH] feat: better subprotocol support and tests --- packages/bruno-requests/src/ws/ws-client.js | 13 +++++- packages/bruno-tests/src/ws/index.js | 31 +++++++++++++- .../ws-test-request-with-subproto.bru | 22 ++++++++++ tests/websockets/subproto.spec.ts | 41 +++++++++++++++++++ 4 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 tests/websockets/fixtures/collection/ws-test-request-with-subproto.bru create mode 100644 tests/websockets/subproto.spec.ts diff --git a/packages/bruno-requests/src/ws/ws-client.js b/packages/bruno-requests/src/ws/ws-client.js index 33757c22a..005da3b56 100644 --- a/packages/bruno-requests/src/ws/ws-client.js +++ b/packages/bruno-requests/src/ws/ws-client.js @@ -88,11 +88,20 @@ class WsClient { try { // Create WebSocket connection - const wsConnection = new ws.WebSocket(parsedUrl.fullUrl, { + const protocols = [].concat([headers['Sec-WebSocket-Protocol'], headers['sec-websocket-protocol']]).filter(Boolean); + const protocolVersion = headers['Sec-WebSocket-Version'] || headers['sec-websocket-version']; + + const wsOptions = { headers, handshakeTimeout: timeout, followRedirects: true, - }); + }; + + if (protocolVersion) { + wsOptions.protocolVersion = protocolVersion; + } + + const wsConnection = new ws.WebSocket(parsedUrl.fullUrl, protocols, wsOptions); // Set up event handlers this.#setupWsEventHandlers(wsConnection, requestId, collectionUid, { keepAlive, keepAliveInterval }); diff --git a/packages/bruno-tests/src/ws/index.js b/packages/bruno-tests/src/ws/index.js index 777cdecf8..c86f70354 100644 --- a/packages/bruno-tests/src/ws/index.js +++ b/packages/bruno-tests/src/ws/index.js @@ -5,7 +5,16 @@ const onSocketError = (err) => { }; const wss = new ws.Server({ - noServer: true + noServer: true, + handleProtocols: (protocols, request) => { + if (request.url == '/ws/sub-proto') { + if (protocols.has("soap")) { + return 'soap' + } + return false + } + return false + } }); wss.on('connection', function connection(ws, request) { @@ -31,7 +40,7 @@ wss.on('connection', function connection(ws, request) { const wsRouter = (request, socket, head) => { socket.on('error', onSocketError); - if (request.url !== '/ws') { + if (!request.url.startsWith('/ws')) { socket.write('HTTP/1.1 404 Not Found\r\n\r\n'); socket.destroy(); @@ -39,6 +48,24 @@ const wsRouter = (request, socket, head) => { return; } + if (request.url == '/ws/sub-proto') { + const subproto = request.headers["sec-websocket-protocol"] || request.headers["Sec-WebSocket-Protocol"] + if (subproto != "soap") { + const message = "Unsupported WebSocket subprotocol" + socket.write( + 'HTTP/1.1 400 Bad Request\r\n' + + 'Content-Type: text/plain\r\n' + + `Content-Length: ${Buffer.byteLength(message)}\r\n` + + 'Connection: close\r\n' + + '\r\n' + + message + ); + socket.destroy(); + socket.removeListener('error', onSocketError); + return + } + } + wss.handleUpgrade(request, socket, head, function done(ws) { wss.emit('connection', ws, request); }); diff --git a/tests/websockets/fixtures/collection/ws-test-request-with-subproto.bru b/tests/websockets/fixtures/collection/ws-test-request-with-subproto.bru new file mode 100644 index 000000000..2af244732 --- /dev/null +++ b/tests/websockets/fixtures/collection/ws-test-request-with-subproto.bru @@ -0,0 +1,22 @@ +meta { + name: ws-test-request-with-subproto + type: ws + seq: 3 +} + +ws { + url: ws://localhost:8081/ws/sub-proto + body: ws + auth: inherit +} + +headers { + Sec-WebSocket-Protocol: soap +} + +body:ws { + name: message 1 + content: ''' + {} + ''' +} diff --git a/tests/websockets/subproto.spec.ts b/tests/websockets/subproto.spec.ts new file mode 100644 index 000000000..0832f501f --- /dev/null +++ b/tests/websockets/subproto.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '../../playwright'; +import { buildCommonLocators } from './lib/locators'; + +const BRU_FILE_NAME = /^ws-test-request-with-subproto$/; + +test.describe.serial('headers', () => { + test('headers are returned if passed', async ({ pageWithUserData: page, restartApp }) => { + const locators = buildCommonLocators(page); + const clearText = async (text: string) => { + for (let i = text.length; i > 0; i--) { + await page.keyboard.press('Backspace'); + } + }; + const originalProtocol = 'soap'; + const wrongProtocol = 'wap'; + + await page.locator('#sidebar-collection-name').click(); + await page.getByTitle(BRU_FILE_NAME).click(); + await page.getByRole('tab', { name: 'Headers1' }).click(); + + await expect(page.locator('pre').filter({ hasText: originalProtocol })).toBeAttached(); + await locators.runner().click(); + + const messages = await locators.messages(); + expect(await messages[0].locator('.text-ellipsis').innerText()).toMatch(/^(Connected to)/); + + await locators.connectionControls.disconnect().click(); + + await page.locator('pre').filter({ hasText: originalProtocol }).click(); + await clearText(originalProtocol); + await page.keyboard.insertText(wrongProtocol); + + await locators.runner().click(); + expect(await messages[0].locator('.text-ellipsis').innerText()).toMatch(/^(Unexpected server response: 400)/); + + await page.locator('pre').filter({ hasText: wrongProtocol }).click(); + await clearText(wrongProtocol); + await page.keyboard.insertText(originalProtocol); + await locators.saveButton().click(); + }); +});