[GH-ISSUE #7410] [Bug] Redirected request steps omit the promised Location from network evidence #4415

Open
opened 2026-10-31 00:49:37 +00:00 by bsakaguchi · 0 comments
Owner

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

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

A source-run capture-browser-scenario CLI command with a GET request step to a loopback endpoint returning 302 Location: /destination fails the step as intended, but its network evidence contains no response event, redirect status, or Location header. This happens with network events and header values explicitly selected. The step error nevertheless says the capture records the redirect and its Location.

I reproduced this in three independent browser launches. Each redirect transaction contains only request, request-side network-content, and request-failed with net::ERR_ABORTED. Every run reports zero dropped events and empty omission ledgers. A navigation control against the same fixture captures status 302 and Location correctly.

Reproduction

Use a source checkout at the baseline below, Node.js 24.22.1, and its locked dependencies (npm ci for a fresh clone). Set REA_BROWSER_EXECUTABLE to an installed Chrome-family executable. The tested browser was Chromium 158.0.8122.0. Run the following from the repository root. This uses the repository's documented source loader and real CLI command registration, so no runtime build is required. It creates a loopback fixture, captures the request case and a navigation control, writes their complete JSON results, and closes the fixture and browsers.

work=$(mktemp -d)
mkdir -p "$work/home"
cat > "$work/reproduce.mjs" <<'EOF'
import { createServer } from 'node:http';
import { writeFile } from 'node:fs/promises';
import { pathToFileURL } from 'node:url';
const root = pathToFileURL(`${process.cwd()}/`);
const { importVerifierSource } = await import(new URL('scripts/lib/verifier-source-loader.mjs', root));
const { createCli, closeCliJavaScriptResources } = await importVerifierSource(new URL('src/cli.ts', root));
const hits = [];
const server = createServer((req, res) => {
  hits.push(req.url);
  if (req.url === '/redirect') {
    res.writeHead(302, { Location: '/destination' });
    res.end();
  } else {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('<!doctype html><p>Fixture</p>');
  }
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const origin = `http://127.0.0.1:${server.address().port}`;
try {
  for (const mode of ['request', 'goto']) {
    hits.length = 0;
    const action = mode === 'request'
      ? { step_id: 'redirect', action: 'request', method: 'GET', path: '/redirect', expected_status: [200] }
      : { step_id: 'redirect', action: 'goto', destination: { url: `${origin}/redirect` }, wait_until: 'load' };
    const scenario = {
      browser: { mode: 'launch', executable_path: process.env.REA_BROWSER_EXECUTABLE },
      start_url: { url: `${origin}/` },
      actions: [action],
      capture: { events: ['network'], network: { header_values: true } }
    };
    let stdout = '';
    await createCli(process.env).serve(
      ['capture-browser-scenario', JSON.stringify(scenario), '--json'],
      { stdout: text => { stdout += text; } }
    );
    await writeFile(new URL(`${mode}-capture.json`, import.meta.url), stdout);
    const capture = JSON.parse(stdout).normalized_result;
    if (!capture) throw new Error(stdout);
    const events = capture.events.items;
    const id = events.find(e => e.kind === 'request' && e.url.url === `${origin}/redirect`).transaction_id;
    const transaction = events.filter(e => e.transaction_id === id);
    console.log(JSON.stringify({
      mode, browser: capture.browser.version,
      stepStatus: capture.steps.at(-1).status,
      kinds: transaction.map(e => e.kind),
      responseStatuses: transaction.filter(e => e.kind === 'response').map(e => e.status),
      locationHeaders: transaction.filter(e => e.kind === 'network-content' && e.phase === 'response')
        .flatMap(e => e.headers.items ?? []).filter(h => h.name.toLowerCase() === 'location'),
      destinationHits: hits.filter(path => path === '/destination').length,
      dropped: capture.events.dropped, omissions: capture.events.omissions,
      error: capture.steps.at(-1).error
    }));
  }
} finally {
  await closeCliJavaScriptResources();
  server.closeAllConnections();
  await new Promise(resolve => server.close(resolve));
}
EOF
HOME="$work/home" TMPDIR="$work" node "$work/reproduce.mjs"

The request case returns a failed step with empty response status and Location lists. Its destination hit count is zero. The navigation control completes, returns response status 302 and Location /destination, and reaches the destination once. The files request-capture.json and goto-capture.json are written beside the script; inspect their normalized_result.events.items and the request step's error.

Expected behavior

Expected source: docs/browser-scenario-contract.md:411 states that request steps fail on redirects without following them and, with header values selected, retain the redirect's Location. The network contract at docs/browser-scenario-contract.md:607 also includes response metadata. This valid same-origin GET scenario selects both network events and header values, so the redirect response and Location should remain available in the capture while the step fails and the redirect remains unfollowed. The diagnostic at src/browser/PlaywrightScenarioRequest.ts:439 promises the same evidence.

Impact and scope

P2: the caller loses consequential network evidence for the tested request-step redirect, and the diagnostic directs them to a Location value absent from the result. This prevents finding the next request path from this capture. The action's failure and no-follow behavior are correct. Scope directly established here is GET with HTTP 302 in launched Chromium, reproduced three times; write methods, other redirect statuses, attached browsers, and other browser versions were not tested. A goto control retains the response but follows the redirect, so it does not establish an equivalent workaround for request steps. No severe harm or security-boundary violation was observed.

REA version

6.8.0; baseline f8f7ac96de, executed from source.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; Darwin 27.0.0; arm64; Chromium 158.0.8122.0.

Target and artifact context

An in-process loopback HTTP fixture and a provider-owned headless Chrome-family browser. The artifact is browser-scenario Evidence with selected network metadata and header values.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

The request-case summary from the source CLI replay was:

{
  "mode": "request",
  "stepStatus": "failed",
  "kinds": ["request", "network-content", "request-failed"],
  "responseStatuses": [],
  "locationHeaders": [],
  "destinationHits": 0,
  "dropped": 0,
  "omissions": {"by_kind": [], "network_transactions": [], "network_transaction_range": null}
}

The step error includes: “The capture records the redirect and, with header values selected, its Location.” The navigation control returned responseStatuses: [302] and the captured header Location: /destination, with zero dropped events and empty omissions. Both final captures report browser cleanup as terminated-owned-process.

Failure source: src/cliBrowserScenarioCommands.ts:66 selects the production provider; src/browser/PlaywrightScenarioRequest.ts:423 installs the request dispatch route and src/browser/PlaywrightScenarioRequest.ts:437 handles the manual redirect result. Network response recording depends on Playwright context response events at src/browser/PlaywrightScenarioEvents.ts:178, with response metadata retained by src/browser/network/PlaywrightScenarioNetwork.ts:215.

Analysis or suspected cause

Source-derived: request steps use a page fetch with manual redirect handling while a temporary Playwright route guards dispatch. The page reports a redirect, but the resulting REA transaction lacks a response event. A route-related change in browser response-event delivery is a plausible mechanism; this verification establishes the REA output defect and the navigation comparison, without independently isolating the Playwright-level cause.

Additional context

Reproduced three times in one environment (GET, HTTP 302, launched Chromium). Write methods, other redirect statuses and attached browsers were not tested.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7410 ### 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 A source-run `capture-browser-scenario` CLI command with a GET request step to a loopback endpoint returning `302 Location: /destination` fails the step as intended, but its network evidence contains no response event, redirect status, or Location header. This happens with network events and header values explicitly selected. The step error nevertheless says the capture records the redirect and its Location. I reproduced this in three independent browser launches. Each redirect transaction contains only `request`, request-side `network-content`, and `request-failed` with `net::ERR_ABORTED`. Every run reports zero dropped events and empty omission ledgers. A navigation control against the same fixture captures status 302 and Location correctly. ### Reproduction Use a source checkout at the baseline below, Node.js 24.22.1, and its locked dependencies (`npm ci` for a fresh clone). Set `REA_BROWSER_EXECUTABLE` to an installed Chrome-family executable. The tested browser was Chromium 158.0.8122.0. Run the following from the repository root. This uses the repository's documented source loader and real CLI command registration, so no runtime build is required. It creates a loopback fixture, captures the request case and a navigation control, writes their complete JSON results, and closes the fixture and browsers. ```sh work=$(mktemp -d) mkdir -p "$work/home" cat > "$work/reproduce.mjs" <<'EOF' import { createServer } from 'node:http'; import { writeFile } from 'node:fs/promises'; import { pathToFileURL } from 'node:url'; const root = pathToFileURL(`${process.cwd()}/`); const { importVerifierSource } = await import(new URL('scripts/lib/verifier-source-loader.mjs', root)); const { createCli, closeCliJavaScriptResources } = await importVerifierSource(new URL('src/cli.ts', root)); const hits = []; const server = createServer((req, res) => { hits.push(req.url); if (req.url === '/redirect') { res.writeHead(302, { Location: '/destination' }); res.end(); } else { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end('<!doctype html><p>Fixture</p>'); } }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); const origin = `http://127.0.0.1:${server.address().port}`; try { for (const mode of ['request', 'goto']) { hits.length = 0; const action = mode === 'request' ? { step_id: 'redirect', action: 'request', method: 'GET', path: '/redirect', expected_status: [200] } : { step_id: 'redirect', action: 'goto', destination: { url: `${origin}/redirect` }, wait_until: 'load' }; const scenario = { browser: { mode: 'launch', executable_path: process.env.REA_BROWSER_EXECUTABLE }, start_url: { url: `${origin}/` }, actions: [action], capture: { events: ['network'], network: { header_values: true } } }; let stdout = ''; await createCli(process.env).serve( ['capture-browser-scenario', JSON.stringify(scenario), '--json'], { stdout: text => { stdout += text; } } ); await writeFile(new URL(`${mode}-capture.json`, import.meta.url), stdout); const capture = JSON.parse(stdout).normalized_result; if (!capture) throw new Error(stdout); const events = capture.events.items; const id = events.find(e => e.kind === 'request' && e.url.url === `${origin}/redirect`).transaction_id; const transaction = events.filter(e => e.transaction_id === id); console.log(JSON.stringify({ mode, browser: capture.browser.version, stepStatus: capture.steps.at(-1).status, kinds: transaction.map(e => e.kind), responseStatuses: transaction.filter(e => e.kind === 'response').map(e => e.status), locationHeaders: transaction.filter(e => e.kind === 'network-content' && e.phase === 'response') .flatMap(e => e.headers.items ?? []).filter(h => h.name.toLowerCase() === 'location'), destinationHits: hits.filter(path => path === '/destination').length, dropped: capture.events.dropped, omissions: capture.events.omissions, error: capture.steps.at(-1).error })); } } finally { await closeCliJavaScriptResources(); server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); } EOF HOME="$work/home" TMPDIR="$work" node "$work/reproduce.mjs" ``` The request case returns a failed step with empty response status and Location lists. Its destination hit count is zero. The navigation control completes, returns response status 302 and Location `/destination`, and reaches the destination once. The files `request-capture.json` and `goto-capture.json` are written beside the script; inspect their `normalized_result.events.items` and the request step's error. ### Expected behavior Expected source: `docs/browser-scenario-contract.md:411` states that request steps fail on redirects without following them and, with header values selected, retain the redirect's Location. The network contract at `docs/browser-scenario-contract.md:607` also includes response metadata. This valid same-origin GET scenario selects both network events and header values, so the redirect response and Location should remain available in the capture while the step fails and the redirect remains unfollowed. The diagnostic at `src/browser/PlaywrightScenarioRequest.ts:439` promises the same evidence. ### Impact and scope P2: the caller loses consequential network evidence for the tested request-step redirect, and the diagnostic directs them to a Location value absent from the result. This prevents finding the next request path from this capture. The action's failure and no-follow behavior are correct. Scope directly established here is GET with HTTP 302 in launched Chromium, reproduced three times; write methods, other redirect statuses, attached browsers, and other browser versions were not tested. A goto control retains the response but follows the redirect, so it does not establish an equivalent workaround for request steps. No severe harm or security-boundary violation was observed. ### REA version 6.8.0; baseline f8f7ac96de8a66a4ab9a23d87e04779bfcd11045, executed from source. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1; Darwin 27.0.0; arm64; Chromium 158.0.8122.0. ### Target and artifact context An in-process loopback HTTP fixture and a provider-owned headless Chrome-family browser. The artifact is browser-scenario Evidence with selected network metadata and header values. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence The request-case summary from the source CLI replay was: ```json { "mode": "request", "stepStatus": "failed", "kinds": ["request", "network-content", "request-failed"], "responseStatuses": [], "locationHeaders": [], "destinationHits": 0, "dropped": 0, "omissions": {"by_kind": [], "network_transactions": [], "network_transaction_range": null} } ``` The step error includes: “The capture records the redirect and, with header values selected, its Location.” The navigation control returned `responseStatuses: [302]` and the captured header `Location: /destination`, with zero dropped events and empty omissions. Both final captures report browser cleanup as `terminated-owned-process`. Failure source: `src/cliBrowserScenarioCommands.ts:66` selects the production provider; `src/browser/PlaywrightScenarioRequest.ts:423` installs the request dispatch route and `src/browser/PlaywrightScenarioRequest.ts:437` handles the manual redirect result. Network response recording depends on Playwright context response events at `src/browser/PlaywrightScenarioEvents.ts:178`, with response metadata retained by `src/browser/network/PlaywrightScenarioNetwork.ts:215`. ### Analysis or suspected cause Source-derived: request steps use a page fetch with manual redirect handling while a temporary Playwright route guards dispatch. The page reports a redirect, but the resulting REA transaction lacks a response event. A route-related change in browser response-event delivery is a plausible mechanism; this verification establishes the REA output defect and the navigation comparison, without independently isolating the Playwright-level cause. ### Additional context Reproduced three times in one environment (GET, HTTP 302, launched Chromium). Write methods, other redirect statuses and attached browsers were not tested.
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#4415
No description provided.