fix(desktop): cancel downloads triggered by link-title fetch window

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.
This commit is contained in:
Brooklyn Nicholson 2026-07-03 01:32:27 -05:00 committed by brooklyn!
parent 7e9e13fe55
commit 42bc07d107
3 changed files with 28 additions and 5 deletions

View file

@ -3,8 +3,7 @@
// Hidden BrowserWindow used by tier-2 link-title resolution: when curl can't
// read a page <title> (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 }

View file

@ -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() } }))
})

View file

@ -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
}