Skip to content

fix: detect redirects without replacing the document - #21

Open
bklaric wants to merge 1 commit into
lukapozega:mainfrom
bklaric:fix/redirect-detection-private-network
Open

bklaric wants to merge 1 commit into
lukapozega:mainfrom
bklaric:fix/redirect-detection-private-network

Conversation

@bklaric

@bklaric bklaric commented Sep 14, 2026

Copy link
Copy Markdown

Fixes #20

With the default settings, pages served from a private network address over plain http rendered as an empty shell. Redirect detection fulfilled the navigation with a document fetched from Node. Chromium places a fulfilled document in the public address space, and its local network access checks then blocked the page's scripts and stylesheets. The issue has a reproduction and the console error.

Change

installRedirectDetection no longer fetches and fulfills the document. It opens a CDP session and enables Fetch for Document requests at the response stage.

  • Redirect on the requested URL: it records the status and headers and fails the request with Aborted. goto then rejects, and renderPage already treats that as a redirect result.
  • Any other paused document: it continues the request untouched, so the browser keeps the response it received from the network. Iframes and redirects on other URLs fall under this too.

The destination of a redirect is still never requested.

One case needs the origin asked separately: redirects Chromium makes itself. For an HSTS-preloaded host such as http://github.com/, Chromium never sends the http request. It pauses on a synthetic 307 Internal Redirect marked Non-Authoritative-Reason: HSTS. Reporting that would turn the origin's 301 into a 307, which the Docker smoke test caught. For those, the origin is asked from Node with page.request.fetch(url, { maxRedirects: 0 }), the same request route.fetch made before. Its answer is only read, never given to the page. If the origin does not redirect or cannot be reached, the browser's own redirect is followed.

Other details:

  • URL matching: the requested URL is compared without its fragment, since CDP request URLs never carry one.
  • Header names: they are lowercased, as Playwright reports them, and repeated headers are joined.
  • Session cleanup: the session is detached when the render ends.
  • No CDP session: if one cannot be opened, the navigation goes ahead without detection, as a failed probe did before.
  • Resource blocking: it still uses page.route('**/*'), so the comment about registration order is gone. An integration test covers both interceptions active at once.

Tests

  • Integration:
    • A new an origin on a private network address block serves the fixture from the machine's first private IPv4 address and renders a page whose content comes from an external script. It fails on main and passes here. It is skipped on a machine without a private address; GitHub's hosted runners have one.
    • The same block checks that a redirect from that origin is still reported without fetching the destination.
    • Another test checks that redirect detection and resource blocking work together.
  • Unit: the renderPage and renderer fakes now stand in for a CDP session instead of a route. New cases cover:
    • continuing a non-redirect, and not replacing it
    • header normalisation
    • a redirect in another document
    • URL fragments
    • detaching the session
    • a session that cannot be opened
    • a paused request that cannot be released
    • the internal-redirect probe, when the origin redirects, doesn't redirect, or can't be reached

npm run lint, typecheck, format:check, test:coverage, build, check:packaging, test:integration and scripts/docker-smoke.sh all pass locally. I also checked the built image against a real app behind Caddy on a Docker network: its pages now render with the default settings, and a real origin's 308 to https is reported as the origin sent it.

A changeset marks this as a patch.

Redirect detection fetched the document from Node and fulfilled the
navigation with it. A fulfilled document has no remote address, so
Chromium places it in the public address space, and its local network
access checks then block every request the page makes to an origin on a
private address. Rendering a Docker service name or any other internal
host over plain http returned the page shell with a 200 and no content.
Loopback is exempt from those checks, which is why the integration
suite never saw it.

The document request is now paused through CDP at the response stage
and released untouched unless it is a redirect, so the browser receives
it from the network. Redirects Chromium makes itself, such as an HSTS
upgrade, are asked of the origin from Node so the origin's own status
is still what gets reported.
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.

Renders from an origin on a private network address come back empty unless followRedirects is on

1 participant