From 4beffc2cf94443239bbb3ca2de8d9cc475356c5f Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Mon, 6 Apr 2026 05:43:09 -0700 Subject: [PATCH] fix: guard openSession against empty name from unreachable federation tiles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking an unreachable remote device tile passed empty session name to openSession(), producing /api/federation/2/connect/ (no session name) → 405. Fix: bail early if name is empty or whitespace. Also added guard in grid click handlers to skip error/status tiles entirely, preventing clicks on unreachable or auth_failed tiles. --- muxplex/frontend/app.js | 5 ++++ muxplex/frontend/tests/test_app.mjs | 46 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/muxplex/frontend/app.js b/muxplex/frontend/app.js index d5613cd..6d2dd03 100644 --- a/muxplex/frontend/app.js +++ b/muxplex/frontend/app.js @@ -839,10 +839,14 @@ function renderGrid(sessions) { on(tile, 'click', (e) => { // Don't navigate when clicking the delete button inside the tile if (e.target.closest && e.target.closest('.tile-delete')) return; + // Don't open error/status tiles (unreachable, auth_failed) + if (tile.classList.contains('source-tile--error') || !tile.dataset.session) return; openSession(tile.dataset.session, { remoteId: tile.dataset.remoteId || '' }); }); on(tile, 'keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { + // Don't open error/status tiles (unreachable, auth_failed) + if (tile.classList.contains('source-tile--error') || !tile.dataset.session) return; openSession(tile.dataset.session, { remoteId: tile.dataset.remoteId || '' }); } }); @@ -1154,6 +1158,7 @@ function updatePageTitle() { * @returns {Promise} */ async function openSession(name, opts = {}) { + if (!name || !name.trim()) return; hidePreview(); _viewingSession = name; _viewingRemoteId = opts.remoteId != null ? opts.remoteId : ''; diff --git a/muxplex/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs index ead44ba..147549e 100644 --- a/muxplex/frontend/tests/test_app.mjs +++ b/muxplex/frontend/tests/test_app.mjs @@ -1240,6 +1240,52 @@ test('openSession for local session still POSTs to local /api/sessions/{name}/co globalThis.setTimeout = origSetTimeout; }); +test('openSession bails early when name is empty string', async () => { + const fetchCalls = []; + const origFetch = globalThis.fetch; + const origGetById = globalThis.document.getElementById; + const origQS = globalThis.document.querySelector; + const origSetTimeout = globalThis.setTimeout; + globalThis.fetch = async (url, opts) => { fetchCalls.push({ url, opts }); return { ok: true }; }; + globalThis.document.getElementById = () => ({ textContent: '', style: {}, classList: { remove: () => {}, add: () => {} } }); + globalThis.document.querySelector = () => null; + globalThis.setTimeout = () => {}; + globalThis.window._openTerminal = () => {}; + + await app.openSession('', {}); + + // Should NOT make any fetch calls to /connect + const connectCall = fetchCalls.find((c) => c.url && c.url.includes('/connect')); + assert.ok(!connectCall, 'should NOT call connect when name is empty string'); + globalThis.fetch = origFetch; + globalThis.document.getElementById = origGetById; + globalThis.document.querySelector = origQS; + globalThis.setTimeout = origSetTimeout; +}); + +test('openSession bails early when name is whitespace only', async () => { + const fetchCalls = []; + const origFetch = globalThis.fetch; + const origGetById = globalThis.document.getElementById; + const origQS = globalThis.document.querySelector; + const origSetTimeout = globalThis.setTimeout; + globalThis.fetch = async (url, opts) => { fetchCalls.push({ url, opts }); return { ok: true }; }; + globalThis.document.getElementById = () => ({ textContent: '', style: {}, classList: { remove: () => {}, add: () => {} } }); + globalThis.document.querySelector = () => null; + globalThis.setTimeout = () => {}; + globalThis.window._openTerminal = () => {}; + + await app.openSession(' \t\n ', {}); + + // Should NOT make any fetch calls to /connect + const connectCall = fetchCalls.find((c) => c.url && c.url.includes('/connect')); + assert.ok(!connectCall, 'should NOT call connect when name is whitespace only'); + globalThis.fetch = origFetch; + globalThis.document.getElementById = origGetById; + globalThis.document.querySelector = origQS; + globalThis.setTimeout = origSetTimeout; +}); + // --- closeSession --- test('closeSession is exported', () => {