Skip to content

fix(engine): hold the render page to the asset download address rules - #7748

Merged
miguel-heygen merged 2 commits into
mainfrom
fix/render-page-address-rules
Nov 14, 2026
Merged

miguel-heygen merged 2 commits into
mainfrom
fix/render-page-address-rules

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

A render service can now hold the render page to the same address rules that asset downloads follow. With
PRODUCER_RESTRICT_PAGE_NETWORK=true, the page reaches only its own project server and public addresses. The
check covers the literal host and every address a name resolves to. Unset (the default), nothing changes, so
local CLI and Studio renders that load from your own machine or network keep working.

Why

Asset downloads already refuse loopback, private, link-local and other special-use addresses, checking resolved
names as well (packages/engine/src/utils/networkPolicy.ts, urlDownloader.ts). The render page itself had no
such rule: anything the page requested went straight out from the render host. A service that renders other
people's projects needs the page to follow the same list.

Related work

Refs the shared special-use address list from #6853. A follow-up applies the rule browser-wide (see "Not covered
yet").

How

  • services/pageNetworkPolicy.ts: a CDP Fetch session on the page, the same pattern videoFrameTransport.ts
    uses. Each paused request continues when it targets the page's own origin, or a host that is public both as
    written and as resolved. Anything else fails with BlockedByClient. Verdicts are cached per host for the page.
    A host that does not resolve is refused.
  • config.ts: resolveRestrictPageNetwork() reads PRODUCER_RESTRICT_PAGE_NETWORK, beside
    resolvePageNavigationTimeout(). It describes the host, not one render, so it stays out of EngineConfig
    snapshots: plans made by an older version still validate.
  • frameCapture.ts: both pages that load a composition (the capture session page and the static verification
    page) install the filter before they navigate. Distributed chunks and the reference-background step use the
    capture session, so they are covered too.
  • Other newPage() sites are not composition pages in the render path: hdrCapture.ts and the producer's
    verifyStaticPage.ts have no render-path caller, and audioFxRender.ts loads the engine's own file:// host page.
  • A refused request reaches the existing diagnostics as net::ERR_BLOCKED_BY_CLIENT, like any failed asset.
  • docs/packages/producer.mdx: one paragraph under "Build a render service".

Not covered yet

This slice filters requests made on the page's own target: the document, its frames (the render browser runs
frames in-process), dedicated workers, redirects, fetch/XHR and every subresource. These are not covered yet
and move to the browser-wide follow-up:

  • service workers
  • shared workers
  • WebSocket connections
  • WebRTC
  • speculative prefetch or prerender, if a composition asks for it

Chrome also does its own name lookup after this check, so a name whose answer changes between the two lookups is
not covered here. The browser-wide follow-up dials the checked address itself.

Test plan

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable)
  • Comments follow CONTRIBUTING.md "Comments": they say why, not what, and a bug fix says what the code must do and how to reproduce the bug

pageNetworkPolicy.browser.test.ts (real headless Chrome): a page served from one local server tries to reach a
second local server by address, by a name that resolves to it, from an iframe, from a dedicated worker and
through a redirect. The second server records every request it receives and must record none. 169.254.169.254
and a name resolving to it are refused within the test's time bound. A name that resolves to a public address
goes through. The page's own server goes through.

frameCapture-pageNetwork.test.ts: with the setting on, both composition pages install the filter before they
navigate; with it off, neither does.

Fails without the fix: pageNetworkPolicy.browser.test.ts with restrictPageNetwork replaced by a no-op fails
5 of 7. frameCapture-pageNetwork.test.ts with the guard replaced by a no-op fails 1 of 2.

Both new test files pass 3 runs in a row; the full engine suite, tsc, oxlint and oxfmt --check pass.

Real renders from this branch on Linux, CLI from source, two runs each:

Fixture Setting off Setting on Frames off vs on
iframe-render-compat 9.8 s, 8.9 s 10.5 s, 8.7 s identical (PSNR inf)
render-video-overlay-stretch (video frames through the page) 6.0 s, 5.7 s 5.7 s, 5.7 s identical (PSNR inf)

A copy of iframe-render-compat whose page fetches http://169.254.169.254/ renders normally with the setting
on; the fetch fails with net::ERR_BLOCKED_BY_CLIENT and shows in the browser diagnostics.

Not exercised: Windows and macOS hosts, and the distributed chunk path end to end (it uses the same capture
session page, covered by the wiring test).

With PRODUCER_RESTRICT_PAGE_NETWORK=true the render page reaches only its own project server and public addresses, checked by host and by resolved address, the rule asset downloads already follow. Unset, nothing changes for local renders.
@mintlify

mintlify Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Nov 14, 2026, 4:21 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

@github-actions

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 116 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

Unstable (1)

  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: APPROVE · 0 blocker · 0 important · 1 nit · head a6037e14

The setting is off by default, so local renders are unchanged. All CI on this head is green, Windows render verification included.

Scope

Read all 7 files. Checked that both composition pages (frameCapture.ts:1682, :4143) install the filter before goto, and that the page origin equals serverUrl (navigation uses ${serverUrl}/index.html). Checked that videoFrameTransport's frame URLs sit on the page's own origin, so the second Fetch session lets them through, and that the render browser launches without site isolation (browserManager.ts:1189), so iframes stay on the page target. Ran both new test files locally against chrome-headless-shell 157, twice: 9/9 pass. With restrictPageNetwork turned into a no-op, 6 fail (5 of 7 + 1 of 2), which matches the fails-without-the-fix line.

let verdict = verdicts.get(target.hostname);
if (!verdict) {
verdict = isPublicHost(target.hostname, resolveHost);
verdicts.set(target.hostname, verdict);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: a rejected lookup is cached as a refusal for the whole page. One DNS blip on a public CDN (fonts, a remote video) would then block every later request to that host for the rest of a long render, which Chrome alone would retry. Consider dropping the cache entry when resolveHost throws, so only refusals of resolved addresses stick.

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit 29533f6 Nov 14, 2026
93 checks passed
@miguel-heygen
miguel-heygen deleted the fix/render-page-address-rules branch November 14, 2026 16:58
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.

1 participant