fix(ui): open media URLs directly in new tabs - #9523
Conversation
lstein
left a comment
There was a problem hiding this comment.
Adversarial review. The fix is correct and does what it claims — I confirmed the premise: before #9163, useMiddleClickOpenInNewTab called openImageInNewTab(imageUrl) → window.open(url, '_blank', 'noopener,noreferrer'), which is exactly what the non-pending branch restores.
But the about:blank-replacement branch introduces a new, avoidable regression of its own. One blocking finding, two smaller ones.
Verification: applied the diff locally, ran vitest run src/features/auth src/features/gallery/videoReviewRegressions.test.ts (41 passed, no type errors) and prettier/eslint on both files (clean). The new test is load-bearing — reverting only the source change makes it fail.
1. The pending branch navigates the new tab twice, unconditionally
const tab = window.open(url, '_blank'); // navigation 1
void waitForMediaCookieSelfHeal().then(() => {
tab?.location.replace(url); // navigation 2 — never conditional
});Triggering sequence:
- Multiuser mode. User logs in — the login response already sets
invokeai_media_token(invokeai/app/api/routers/auth.py:245), so media works immediately. useMediaCookieRefreshmounts,setSelfHealPending(true), andrefreshMediaCookie()hits a transient network error →RETRY_DELAYS_MS[0] = 2000, soselfHealPendingstaystruefor 2s, and up to ~12s across both retries.- Inside that window the user middle-clicks a video in the gallery (
GalleryVideoItem.tsx:130→useMiddleClickOpenInNewTab→openMediaInNewTab). - Pending branch taken. The tab opens, the cookie is valid, the video loads and starts playing.
- The refresh settles — on success, on 401, or on retries exhausted; all three call
setSelfHealPending(false)— andtab.location.replace(url)fires. The tab reloads: playback restarts at 0:00 and the whole file is re-fetched.
Under the old code the tab sat on about:blank, so there was exactly one media load. The replace is only useful if the first load actually 401'd, and nothing checks that — it can't, cross-document. On the 401 and retries-exhausted paths the reload is guaranteed to be a second 401.
Suggested fix: drop the pending branch entirely and always take the direct path. That is what middle-click did before #9163, what ContextMenuItemOpenInNewTab still does for images today via openImageInNewTab, and what this PR's own non-pending branch already accepts. The residual exposure is a broken tab during a sub-second startup window, which the code already tolerates in three other places. If the retry is worth keeping, at minimum skip the replace when the self-heal ended in failure.
2. The non-pending branch duplicates an existing helper
src/common/util/openImageInNewTab.ts is, in its entirety:
export const openImageInNewTab = (imageUrl: string) => {
window.open(imageUrl, '_blank', 'noopener,noreferrer');
};That is character-for-character the line this PR adds, and it is still live (ContextMenuItemOpenInNewTab.tsx:12). Worth collapsing to one definition — especially since resolving finding 1 makes openMediaInNewTab equal to it.
3. No guard against about:blank returning; the pending branch is untested
This PR exists because #9163 quietly swapped in about:blank. The repo already uses source-text regression guards for exactly this class of thing (protectedMediaConsumers.test.ts, and videoReviewRegressions.test.ts, which asserts not.toContain('window.open(videoDTO.video_url')). A one-liner alongside them —
expect(readSource('.../useMediaCookieRefresh.ts')).not.toContain('about:blank');— would actually lock the fix in. As it stands the new test covers only the branch that has no logic; the two-step branch has zero coverage.
Attacks that failed, for the record
noopener,noreferrerforces a popup window instead of a tab — no. MDN carves out exactly these two tokens, andopenImageInNewTabhas shipped this string in production.tab.opener = nullthrows cross-origin — no. Navigation is async, so the handle still points at the same-origin initialabout:blankdocument at assignment time. Moot anyway: media URLs are relative.tab.location.replaceblocked cross-origin — no.replaceis on the cross-origin-accessible property list, and again the URLs are same-origin.- The first 401 pops a browser auth dialog — no. Media routes send
WWW-Authenticate: Bearer(auth_dependencies.py:127-147); browsers only prompt for Basic/Digest/NTLM/Negotiate. - The second navigation triggers a duplicate download — no.
Content-Disposition: inlineon both media routes (images.py:388,videos.py:608), so the tab renders rather than downloads. SameSite=laxcookie dropped because ofnoreferrer— no. SameSite is computed from the initiator site, not theRefererheader; a top-level same-site GET still sends it.- Startup hole where
selfHealPendingis stillfalsebefore the effect runs — real, but identical under the old code (waitForMediaCookieSelfHeal()resolved immediately → same instant 401). Not a regression. - Popup blocked →
tab === null— guarded bytab?., unchanged. - Test global-stub leakage —
afterEachis scoped to the newdescribeandunstubAllGlobalsrestores it. The vitest environment isnode(noenvironmentinvite.config.mts), sowindowgenuinely comes from the stub. - Another
about:blankopener survives the fix —konva/util.ts:460(window.open('')inpreviewBlob) would hit the same OS-delegation issue, but both call sites are gated onmanager._isDebugging. Dev-only, out of scope.
|
Thanks for the thorough review. I agree with the findings and addressed all three in
|
Summary
Fixes a UI regression where middle-clicking gallery media could ask Windows to open an external application instead of opening a browser tab.
The regression was introduced by PR #9163 in commit
eb9a951248775225fdea689caf9044209d6f0829, which changed the middle-click path to openabout:blankbefore navigating to the media URL.This change opens the media URL directly. If media-cookie self-heal is pending, the URL is reloaded after self-heal settles so protected media remains accessible without using an
about:blankintermediary.A focused regression test verifies that the media URL is passed directly to
window.open()when self-heal is not pending.Related Issues / Discussions
Closes #9522
QA Instructions
Automated verification:
pnpm exec vitest run src/features/auth/hooks/useMediaCookieRefresh.test.tspnpm buildMerge Plan
Normal merge. No API, database, schema, Redux, or dependency changes.
Checklist
What's Newcopy (if doing a release after this PR)