[GH-ISSUE #3941] [Bug] Unchanged-target navigation events abort web screenshots #1547

Closed
opened 2026-10-16 21:06:59 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 15, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3941

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Observed behavior

The public capture-web-screenshot command rejects a valid PNG when the selected page emits either a navigation request that never commits or a same-document history event that leaves its URL unchanged. In both reproduced cases, the frame ID, committed loader, actual URL and approved origin remain unchanged throughout capture. The command exits unsuccessfully with the browser reason target_changed instead of returning the screenshot.

Reproduction

Use the baseline under Runtime or environment with its installed dependencies and existing compiled runtime. This reproduction uses the repository's fake browser over real loopback HTTP/WebSocket transport and calls the public CLI. It needs no real browser or external service.

  1. Save the following independently written test as tests/boundary/browser/screenshotNavigationRepro.test.ts. The fake's default frame tree remains unchanged for the two failing cases. The history event deliberately carries the exact current URL.
import { expect, it } from "vitest";
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
import { runCliJson } from "../../fixtures/cliJsonProcess.js";

it.each(["idle", "requested-only", "same-url-history", "replaced-loader"])(
  "capture contract for %s",
  async (kind) => {
    const browser = await startFakeCdpBrowser({
      ...(kind === "replaced-loader"
        ? { screenshotDocumentLoader: "loader-reloaded" }
        : {}),
      commandEvents: ({ method, sessionId }, origin) => {
        if (method !== "Page.captureScreenshot") return undefined;
        if (kind === "requested-only") return [{
          method: "Page.frameRequestedNavigation",
          sessionId,
          params: {
            frameId: "frame-main", url: `${origin}/pending`,
            reason: "scriptInitiated", disposition: "currentTab",
          },
        }];
        if (kind === "same-url-history") return [{
          method: "Page.navigatedWithinDocument",
          sessionId,
          params: {
            frameId: "frame-main",
            url: `${origin}/app?token=frame-secret`,
            navigationType: "historyApi",
          },
        }];
        return undefined;
      },
    });
    try {
      const result = await runCliJson([
        "capture-web-screenshot", browser.endpoint, "allowed-page",
        "--allowed-origins", browser.allowedOrigin, "--json",
      ]);
      if (kind === "replaced-loader") {
        expect(result).toMatchObject({
          code: "target_unavailable", details: { reason: "target_changed" },
        });
      } else {
        expect(result).toMatchObject({
          normalized_result: { artifact: { media_type: "image/png", bytes: 70 } },
        });
      }
    } finally {
      await browser.close();
    }
  },
);
  1. Run the focused test with a disposable home directory. Use the repository's pinned Node.js and npm toolchain.
repro_home=$(mktemp -d)
HOME="$repro_home" NPM_CONFIG_UPDATE_NOTIFIER=false npx vitest run tests/boundary/browser/screenshotNavigationRepro.test.ts
rm -rf "$repro_home"
rm tests/boundary/browser/screenshotNavigationRepro.test.ts
  1. Observe two failing contract assertions, for requested-only and same-url-history. The idle and replaced-loader controls pass. The same test body was executed locally under a temporary test filename and produced exactly this result.

Expected behavior

An authorized screenshot of an unchanged selected target should return the visible viewport PNG. A navigation request without any committed change, or a history event that keeps both document identity and URL unchanged, should not alone establish that the target changed. Capture must continue to reject an actual document replacement or a departure from the approved scope.

This expectation is intentionally limited to unchanged actual URLs and known unchanged document identity. It does not require successful capture across arbitrary SPA URL changes, missing identity information, or committed navigations.

src/contracts/browserToolContracts.ts:263-270 promises the inline viewport PNG. docs/browser-observation.md:249-273 describes discarding mixed results on main-frame navigation. tests/boundary/browser/cdpScreenshotDocumentIdentity.test.ts:61-81 distinguishes unchanged committed events from loader replacement.

Impact and scope

P2. A valid screenshot request explicitly fails when either non-changing event arrives during capture. Each case failed in all three injected captures attempted. Retrying against the same fake after its one-time event succeeded and returned a PNG. This does not guarantee recovery of transient historical pixels or success on continuously active pages. Real-browser incidence is unknown.

REA version

rea 6.5.0 at source commit 4692bb3330

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

Authorized loopback fake CDP page with unchanged frame ID, committed loader, actual URL, and origin; a valid 70-byte PNG response. The URL query in the fixture is synthetic. No live browser tested.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

Both non-changing event cases returned the following meaningful CLI output instead of a screenshot. Separate recorder probes observed exit status 1; the idle control exited 0 and returned a 70-byte PNG.

{
  "error": "Browser observation failed",
  "code": "target_unavailable",
  "category": "unavailable",
  "retryable": false,
  "details": {
    "operation": "capture_web_screenshot",
    "reason": "target_changed"
  }
}

Each non-changing event case failed in all three injected captures attempted locally: two recorder runs and one run of the included contract test. In the recorder probes, before and after frame records had identical frame ID, loader ID, URL and origin. Retrying against the same fake browser after its one-time event succeeded in both cases and returned the PNG. These are controlled simulated-provider results; real-browser incidence and timing frequency were not measured.

Typed outcome or recovery expected

With either non-changing event injected during screenshot capture, the same CLI request must succeed and return the PNG with the unchanged selected target identity and URL. The idle control must remain successful. Actual committed document replacement must still produce target_changed, including replacement at the same URL; origin authorization must remain enforced. The repository's existing screenshot identity tests provide the document-replacement control. These criteria leave the implementation and regression-test design open.

Analysis or suspected cause

Source-derived: src/browser/CdpCaptureEventHelpers.ts:12-25 includes requested and same-document navigation events. src/browser/CdpScreenshot.ts:135-162 exempts an unchanged Page.frameNavigated but sets the changed flag unconditionally for the other two events; lines 103-109 then reject the screenshot.

Additional context

The cited failing code is unchanged on current origin/main at 22ec8bdb2f7d4626587f23b1eed4a207df600386. The required behavior is deliberately confined to unchanged actual URLs and known unchanged document identity. Arbitrary SPA URL changes, unknown loader identity, committed replacements, and origin departures are outside the requested acceptance behavior.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3941 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Process capture or runtime observation ### Observed behavior The public `capture-web-screenshot` command rejects a valid PNG when the selected page emits either a navigation request that never commits or a same-document history event that leaves its URL unchanged. In both reproduced cases, the frame ID, committed loader, actual URL and approved origin remain unchanged throughout capture. The command exits unsuccessfully with the browser reason `target_changed` instead of returning the screenshot. ### Reproduction Use the baseline under Runtime or environment with its installed dependencies and existing compiled runtime. This reproduction uses the repository's fake browser over real loopback HTTP/WebSocket transport and calls the public CLI. It needs no real browser or external service. 1. Save the following independently written test as `tests/boundary/browser/screenshotNavigationRepro.test.ts`. The fake's default frame tree remains unchanged for the two failing cases. The history event deliberately carries the exact current URL. ```ts import { expect, it } from "vitest"; import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; import { runCliJson } from "../../fixtures/cliJsonProcess.js"; it.each(["idle", "requested-only", "same-url-history", "replaced-loader"])( "capture contract for %s", async (kind) => { const browser = await startFakeCdpBrowser({ ...(kind === "replaced-loader" ? { screenshotDocumentLoader: "loader-reloaded" } : {}), commandEvents: ({ method, sessionId }, origin) => { if (method !== "Page.captureScreenshot") return undefined; if (kind === "requested-only") return [{ method: "Page.frameRequestedNavigation", sessionId, params: { frameId: "frame-main", url: `${origin}/pending`, reason: "scriptInitiated", disposition: "currentTab", }, }]; if (kind === "same-url-history") return [{ method: "Page.navigatedWithinDocument", sessionId, params: { frameId: "frame-main", url: `${origin}/app?token=frame-secret`, navigationType: "historyApi", }, }]; return undefined; }, }); try { const result = await runCliJson([ "capture-web-screenshot", browser.endpoint, "allowed-page", "--allowed-origins", browser.allowedOrigin, "--json", ]); if (kind === "replaced-loader") { expect(result).toMatchObject({ code: "target_unavailable", details: { reason: "target_changed" }, }); } else { expect(result).toMatchObject({ normalized_result: { artifact: { media_type: "image/png", bytes: 70 } }, }); } } finally { await browser.close(); } }, ); ``` 2. Run the focused test with a disposable home directory. Use the repository's pinned Node.js and npm toolchain. ```sh repro_home=$(mktemp -d) HOME="$repro_home" NPM_CONFIG_UPDATE_NOTIFIER=false npx vitest run tests/boundary/browser/screenshotNavigationRepro.test.ts rm -rf "$repro_home" rm tests/boundary/browser/screenshotNavigationRepro.test.ts ``` 3. Observe two failing contract assertions, for `requested-only` and `same-url-history`. The idle and replaced-loader controls pass. The same test body was executed locally under a temporary test filename and produced exactly this result. ### Expected behavior An authorized screenshot of an unchanged selected target should return the visible viewport PNG. A navigation request without any committed change, or a history event that keeps both document identity and URL unchanged, should not alone establish that the target changed. Capture must continue to reject an actual document replacement or a departure from the approved scope. This expectation is intentionally limited to unchanged actual URLs and known unchanged document identity. It does not require successful capture across arbitrary SPA URL changes, missing identity information, or committed navigations. `src/contracts/browserToolContracts.ts:263-270` promises the inline viewport PNG. `docs/browser-observation.md:249-273` describes discarding mixed results on main-frame navigation. `tests/boundary/browser/cdpScreenshotDocumentIdentity.test.ts:61-81` distinguishes unchanged committed events from loader replacement. ### Impact and scope P2. A valid screenshot request explicitly fails when either non-changing event arrives during capture. Each case failed in all three injected captures attempted. Retrying against the same fake after its one-time event succeeded and returned a PNG. This does not guarantee recovery of transient historical pixels or success on continuously active pages. Real-browser incidence is unknown. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context Authorized loopback fake CDP page with unchanged frame ID, committed loader, actual URL, and origin; a valid 70-byte PNG response. The URL query in the fixture is synthetic. No live browser tested. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence Both non-changing event cases returned the following meaningful CLI output instead of a screenshot. Separate recorder probes observed exit status 1; the idle control exited 0 and returned a 70-byte PNG. ```json { "error": "Browser observation failed", "code": "target_unavailable", "category": "unavailable", "retryable": false, "details": { "operation": "capture_web_screenshot", "reason": "target_changed" } } ``` Each non-changing event case failed in all three injected captures attempted locally: two recorder runs and one run of the included contract test. In the recorder probes, before and after frame records had identical frame ID, loader ID, URL and origin. Retrying against the same fake browser after its one-time event succeeded in both cases and returned the PNG. These are controlled simulated-provider results; real-browser incidence and timing frequency were not measured. ### Typed outcome or recovery expected With either non-changing event injected during screenshot capture, the same CLI request must succeed and return the PNG with the unchanged selected target identity and URL. The idle control must remain successful. Actual committed document replacement must still produce `target_changed`, including replacement at the same URL; origin authorization must remain enforced. The repository's existing screenshot identity tests provide the document-replacement control. These criteria leave the implementation and regression-test design open. ### Analysis or suspected cause Source-derived: `src/browser/CdpCaptureEventHelpers.ts:12-25` includes requested and same-document navigation events. `src/browser/CdpScreenshot.ts:135-162` exempts an unchanged `Page.frameNavigated` but sets the changed flag unconditionally for the other two events; lines 103-109 then reject the screenshot. ### Additional context The cited failing code is unchanged on current `origin/main` at `22ec8bdb2f7d4626587f23b1eed4a207df600386`. The required behavior is deliberately confined to unchanged actual URLs and known unchanged document identity. Arbitrary SPA URL changes, unknown loader identity, committed replacements, and origin departures are outside the requested acceptance behavior.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1547
No description provided.