From 42bc07d107cf9f932acc5a00c20aafc003737241 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Fri, 3 Jul 2026 01:32:27 -0500 Subject: [PATCH] fix(desktop): cancel downloads triggered by link-title fetch window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hidden BrowserWindow used by fetchLinkTitle to scrape page titles had no will-download handler on its session. When a link artifact URL responds with Content-Disposition: attachment, Electron fires will-download and the file is saved for real — explaining the spurious download on the Artifacts page. Add guardLinkTitleSession() (parallel to the existing audio-mute guard for #49505) that installs a will-download handler which immediately cancels every download item on the hermes:link-titles session. Call it from getLinkTitleSession() right after the request-type blocklist is wired up. --- apps/desktop/electron/link-title-window.cjs | 16 +++++++++++++--- apps/desktop/electron/link-title-window.test.cjs | 14 +++++++++++++- apps/desktop/electron/main.cjs | 3 ++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/apps/desktop/electron/link-title-window.cjs b/apps/desktop/electron/link-title-window.cjs index 80b3af3976e..3aeabcfe611 100644 --- a/apps/desktop/electron/link-title-window.cjs +++ b/apps/desktop/electron/link-title-window.cjs @@ -3,8 +3,7 @@ // Hidden BrowserWindow used by tier-2 link-title resolution: when curl can't // read a page (bot walls, JS-rendered pages), we briefly load the URL // in an offscreen window and read its title. That window loads arbitrary -// user-linked pages — including YouTube/`watch` URLs that autoplay — so it must -// never be allowed to emit sound. +// user-linked pages, so it must never emit sound or trigger real downloads. function linkTitleWindowOptions(partitionSession) { return { @@ -39,4 +38,15 @@ function createLinkTitleWindow(BrowserWindow, partitionSession) { return window } -module.exports = { createLinkTitleWindow, linkTitleWindowOptions } +// Cancel any download the title-fetch window triggers. Without this, a link +// artifact URL served with Content-Disposition: attachment auto-downloads every +// time the Artifacts page renders and fetchLinkTitle loads it. +function guardLinkTitleSession(partitionSession) { + try { + partitionSession.on('will-download', (_event, item) => item.cancel()) + } catch { + // best-effort; worst case is a spurious download + } +} + +module.exports = { createLinkTitleWindow, guardLinkTitleSession, linkTitleWindowOptions } diff --git a/apps/desktop/electron/link-title-window.test.cjs b/apps/desktop/electron/link-title-window.test.cjs index 87333efb69d..64228ce4a8c 100644 --- a/apps/desktop/electron/link-title-window.test.cjs +++ b/apps/desktop/electron/link-title-window.test.cjs @@ -1,7 +1,7 @@ const assert = require('node:assert/strict') const test = require('node:test') -const { createLinkTitleWindow, linkTitleWindowOptions } = require('./link-title-window.cjs') +const { createLinkTitleWindow, guardLinkTitleSession, linkTitleWindowOptions } = require('./link-title-window.cjs') function makeFakeBrowserWindow() { const calls = { audioMuted: [] } @@ -54,3 +54,15 @@ test('createLinkTitleWindow still returns the window if muting throws', () => { assert.ok(window instanceof ThrowingBrowserWindow) }) + +test('guardLinkTitleSession cancels downloads triggered by the title-fetch window', () => { + let cancelled = false + const handlers = {} + guardLinkTitleSession({ on: (e, h) => { handlers[e] = h } }) + handlers['will-download'](null, { cancel: () => { cancelled = true } }) + assert.ok(cancelled) +}) + +test('guardLinkTitleSession is a no-op when session.on throws', () => { + assert.doesNotThrow(() => guardLinkTitleSession({ on() { throw new Error() } })) +}) diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index 64a8701c4a2..29b2891d7d5 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -36,7 +36,7 @@ const { SESSION_WINDOW_MIN_WIDTH } = require('./session-windows.cjs') const { canImportHermesCli, verifyHermesCli } = require('./backend-probes.cjs') -const { createLinkTitleWindow } = require('./link-title-window.cjs') +const { createLinkTitleWindow, guardLinkTitleSession } = require('./link-title-window.cjs') const { probeGatewayWebSocket } = require('./gateway-ws-probe.cjs') const { adoptServedDashboardToken } = require('./dashboard-token.cjs') const { waitForDashboardPortAnnouncement } = require('./backend-ready.cjs') @@ -3509,6 +3509,7 @@ function getLinkTitleSession() { linkTitleSession.webRequest.onBeforeRequest((details, callback) => { callback({ cancel: RENDER_TITLE_BLOCKED_RESOURCES.has(details.resourceType) }) }) + guardLinkTitleSession(linkTitleSession) return linkTitleSession }