Skip to content

fix(ui): open media URLs directly in new tabs - #9523

Open
DustyShoe wants to merge 5 commits into
invoke-ai:mainfrom
DustyShoe:fix/middle-click-open-media
Open

fix(ui): open media URLs directly in new tabs#9523
DustyShoe wants to merge 5 commits into
invoke-ai:mainfrom
DustyShoe:fix/middle-click-open-media

Conversation

@DustyShoe

Copy link
Copy Markdown
Collaborator

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 open about:blank before 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:blank intermediary.

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

  1. Enable "Use Middle Click to Open Images/Videos in New Tab".
  2. Middle-click an image or video in the gallery.
  3. Confirm that the media opens in a browser tab without a Windows application prompt.
  4. Repeat after restoring an authenticated session.

Automated verification:

  • pnpm exec vitest run src/features/auth/hooks/useMediaCookieRefresh.test.ts
  • pnpm build
  • Full frontend suite: 150 test files and 1893 tests passed.

Merge Plan

Normal merge. No API, database, schema, Redux, or dependency changes.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • Changes to a redux slice have a corresponding migration
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

@github-actions github-actions Bot added the frontend PRs that change frontend files label Aug 20, 2026
@lstein lstein self-assigned this Aug 24, 2026
@lstein lstein added the 6.14.1 label Aug 24, 2026
@lstein lstein moved this to 6.14.1: Bug fixes to 6.14.0 in Invoke - Community Roadmap Aug 24, 2026

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Multiuser mode. User logs in — the login response already sets invokeai_media_token (invokeai/app/api/routers/auth.py:245), so media works immediately.
  2. useMediaCookieRefresh mounts, setSelfHealPending(true), and refreshMediaCookie() hits a transient network error → RETRY_DELAYS_MS[0] = 2000, so selfHealPending stays true for 2s, and up to ~12s across both retries.
  3. Inside that window the user middle-clicks a video in the gallery (GalleryVideoItem.tsx:130useMiddleClickOpenInNewTabopenMediaInNewTab).
  4. Pending branch taken. The tab opens, the cookie is valid, the video loads and starts playing.
  5. The refresh settles — on success, on 401, or on retries exhausted; all three call setSelfHealPending(false) — and tab.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,noreferrer forces a popup window instead of a tab — no. MDN carves out exactly these two tokens, and openImageInNewTab has shipped this string in production.
  • tab.opener = null throws cross-origin — no. Navigation is async, so the handle still points at the same-origin initial about:blank document at assignment time. Moot anyway: media URLs are relative.
  • tab.location.replace blocked cross-origin — no. replace is 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: inline on both media routes (images.py:388, videos.py:608), so the tab renders rather than downloads.
  • SameSite=lax cookie dropped because of noreferrer — no. SameSite is computed from the initiator site, not the Referer header; a top-level same-site GET still sends it.
  • Startup hole where selfHealPending is still false before the effect runs — real, but identical under the old code (waitForMediaCookieSelfHeal() resolved immediately → same instant 401). Not a regression.
  • Popup blocked → tab === null — guarded by tab?., unchanged.
  • Test global-stub leakageafterEach is scoped to the new describe and unstubAllGlobals restores it. The vitest environment is node (no environment in vite.config.mts), so window genuinely comes from the stub.
  • Another about:blank opener survives the fixkonva/util.ts:460 (window.open('') in previewBlob) would hit the same OS-delegation issue, but both call sites are gated on manager._isDebugging. Dev-only, out of scope.

@DustyShoe

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review. I agree with the findings and addressed all three in 1d3379811c.

  1. Removed the pending self-heal navigation branch entirely. Media URLs now always open directly, with no unconditional reload or duplicate fetch.
  2. Replaced the separate image/video openers with a single shared openMediaInNewTab helper used by all image and video consumers.
  3. Added regression coverage that:
    • verifies the URL is passed directly to window.open();
    • rejects any about:blank intermediary;
    • ensures the middle-click hook remains wired to the shared direct opener instead of the auth helper.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.14.1 frontend PRs that change frontend files

Projects

Status: 6.14.1: Bug fixes to 6.14.0

Development

Successfully merging this pull request may close these issues.

[bug]: Middle-clicking a gallery image attempts to open a Windows application

2 participants