Skip to content

fix(core): re-host non-http(s) CORS iframe URLs instead of failing upload#2294

Open
Shivanshu-07 wants to merge 3 commits into
masterfrom
fix/PER-9574-blob-iframe-upload
Open

fix(core): re-host non-http(s) CORS iframe URLs instead of failing upload#2294
Shivanshu-07 wants to merge 3 commits into
masterfrom
fix/PER-9574-blob-iframe-upload

Conversation

@Shivanshu-07

@Shivanshu-07 Shivanshu-07 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Problem

Percy CLI fails snapshot upload when a page includes an iframe whose src is a blob: URL:

[percy:core] Error: Invalid URL, scheme must be http or https: blob:https://www.example.com

Regression introduced by #2121, which moved CORS iframe processing into @percy/core. Reported in #2285 (works on 1.31.7, fails on 1.31.14+).

Root cause

processCorsIframesInDomSnapshot() in packages/core/src/utils.js creates an upload resource for every CORS iframe, keyed on the iframe's frameUrl:

const iframeResource = { url: frameUrlWithWidth, content: iframeSnapshot.html, mimetype: 'text/html' };

A blob: URL is an in-memory handle that only exists in the originating browser context — there is no endpoint to fetch, it is scoped to the creating document, and it is revoked on unload. Percy's renderer (a separate browser) can never resolve it. When the iframe src is blob: (also data:, about:), that URL is sent to the API as a resource-url; the API rejects any non-http(s) scheme (snapshot_service.rbInvalid URL, scheme must be http or https), failing the entire snapshot upload.

Fix

@percy/dom already handles blob URLs correctly for stylesheets (serialize-cssom.js): it extracts the content and re-hosts it under a synthetic /__serialized__/ http(s) URL via resourceFromText. The CORS iframe path never applied that pattern — this PR brings it to parity.

For a non-http(s) frameUrl, buildSyntheticFrameResourceUrl() mints a synthetic http(s) URL from the captured iframe HTML:

  • resolved against the page origin (options.url), with localhost/127.0.0.1 rewritten to render.percy.local — matching how serialized DOM resources are hosted;
  • path /__serialized__/cors-iframe-<percyElementId>.html (percyElementId URL-encoded);
  • used for both the upload resource url and the rewritten iframe src, so the renderer serves the captured content exactly as it does an ordinary cross-origin iframe.

The blob URL never reaches the API, the snapshot uploads successfully, and the iframe content is preserved and renderable. Frames without a percyElementId (where the src cannot be rewritten to the synthetic URL) are skipped so no resource is orphaned. http(s) frames are completely unchanged.

Tests

packages/core/test/utils.test.js:

  • isHttpOrHttpsUrl, rewriteLocalhostURL, buildSyntheticFrameResourceUrl helpers (origin resolution, localhost rewrite, render.percy.local fallback, id encoding, null when no percyElementId)
  • processCorsIframesInDomSnapshot: re-hosts a blob frameUrl (resource + src rewritten, no blob: left), re-hosts data/about against render.percy.local fallback, skips a non-http(s) frame with no percyElementId, mixed http+blob array

All 121 utils specs and all Snapshot › CORS iframe processing integration specs pass; lint clean; unreachable defensive catch marked with istanbul ignore.

Fixes PER-9574 / closes #2285

🤖 Generated with Claude Code

Shivanshu-07 and others added 2 commits June 16, 2026 18:56
…s (PER-8604, PER-8608)

PER-8608 (CWE-829) — every third-party action across all 11 workflows was
pinned to a mutable tag, allowing a hijacked/retagged action to inject code
into CI (which handles signing keys and publish tokens). Pin every `uses:` to
an immutable 40-char commit SHA (tag preserved in a trailing comment):
checkout, setup-node, cache, upload-artifact, download-artifact, stale,
github-script, action-regex-match, pull-request-comment-branch,
gha-jobid-action, winterjung/split, trigger-workflow-and-wait,
create-pull-request.

PER-8604 (CWE-732) — workflows ran with the implicit write-all GITHUB_TOKEN.
Add a top-level `permissions: contents: read` to every workflow and minimal
job-level grants only where required:
  - executable.yml (build, notify): contents: write — upload release assets
  - stale.yml: issues: write, pull-requests: write
  - sdk-regression.yml: statuses: write + pull-requests: read

Also re PER-8610: sdk-regression.yml is issue_comment-triggered but already
gates execution on an author-permission check (write/admin collaborators only);
the least-privilege block above further limits the token exposed to that flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ilure

PR #2121 moved CORS iframe processing into core. For every CORS iframe,
processCorsIframesInDomSnapshot() creates an upload resource keyed on the
iframe's frameUrl. When an iframe src is a blob: URL (also data:, about:,
etc.), that URL is sent to the API as a resource-url. The API rejects any
resource URL whose scheme is not http/https
(snapshot_service.rb: "Invalid URL, scheme must be http or https"), which
fails the entire snapshot upload — a regression from pre-#2121 behavior.

Skip CORS iframe entries whose frameUrl is not http(s): no bad resource is
created and the iframe HTML is left untouched, so the snapshot uploads
successfully instead of hard-failing. Percy cannot fetch/serve blob URLs
anyway since they are ephemeral and browser-context specific.

Fixes PER-9574 / #2285

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07 Shivanshu-07 requested a review from a team as a code owner June 17, 2026 04:21
…g it

Upgrade the previous skip-based fix to preserve the iframe. @percy/dom
already handles blob URLs for stylesheets by extracting the content and
re-hosting it under a synthetic /__serialized__/ http(s) URL
(serialize-cssom.js); the CORS iframe path never applied that pattern.

For a non-http(s) frameUrl, mint a synthetic http(s) URL from the captured
iframe HTML (buildSyntheticFrameResourceUrl), resolved against the page
origin with localhost rewritten to render.percy.local, then use it for both
the upload resource and the rewritten iframe src. The blob URL never reaches
the API, the snapshot uploads, and the iframe content is preserved and
renderable. Frames without a percyElementId (where the src cannot be
rewritten) are still skipped so no resource is orphaned. http(s) frames are
unchanged.

PER-9574 / #2285

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Shivanshu-07 Shivanshu-07 changed the title fix(core): skip non-http(s) CORS iframe frame URLs to avoid upload failure fix(core): re-host non-http(s) CORS iframe URLs instead of failing upload Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Regression: Snapshot upload fails on blob iframe URL after CORS iframe processing change

1 participant