Skip to content

fix(server): a driven request body that was cut short says so - #781

Merged
divshekhar merged 3 commits into
reticlehq:mainfrom
thegoodengineer:fix/wire-body-truncation-report
Sep 6, 2026
Merged

fix(server): a driven request body that was cut short says so#781
divshekhar merged 3 commits into
reticlehq:mainfrom
thegoodengineer:fix/wire-body-truncation-report

Conversation

@thegoodengineer

@thegoodengineer thegoodengineer commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

What & why

Coding rule 12: a transform that can drop or truncate data on a path an agent reads must report that it did, in a machine-readable way. buildNetworkDetail caps the request body it takes off the network stack at 8,192 characters and says nothing.

The in-page observer caps the same body and does say so. projectBody returns { body, truncated } and the NET_REQUEST carries requestBodyTruncated: true. So the two capture routes disagreed about whether a shortened body announces itself, and the driven route is the one whose body overwrites the other in mergeNetworkDetail, by design, because when they disagree the disagreement is the finding.

That makes the failure concrete rather than theoretical. A request whose in-page body was complete, merged with a wire body that was cut, produced an event that read as a whole body and was not. An agent asserting over that payload gets an absence of the thing that was cut off, which is the false-green shape this repo has already paid for three times. The reverse case leaves a stale requestBodyTruncated: true caveating a body that is now whole.

The cap itself is right, and I have not touched it. Only the silence.

Why the guard did not catch it

scripts/check-lossy-transforms.mjs is honest about its own limit: it catches a new export in a registered module, not a new behaviour, and packages/server/src/input/network-detail.ts was never registered. It is now, with every export classified, so the next person to add a transform here is made to answer the question.

No issue filed; found while reading the driven capture path.

How it was verified

packages/server/src/input/network-detail.lossy-conformance.test.ts, written RED first, on a fixture guaranteed to exceed the bound:

  • a cut body reports requestBodyTruncated: true
  • a body that fits carries no such field, so the caveat means something when it appears
  • the merged event is marked truncated when the wire body that replaced the page body was cut
  • the stale caveat is cleared when the wire body that replaced it is whole

Three of the four fail on main. network-detail.test.ts passes unchanged.

Guard: node scripts/check-lossy-transforms.mjs reports 40 exports across 7 read-path modules, and --self-test still catches all 12 synthetic problems.

One classification worth arguing with

I marked attachNetworkDetail as silent rather than quietly leaving it out. It drops a response whose headers() rejects, which happens when the page or CDP session is closing, and the existing comment explains why: on the stdio start() path an unhandled rejection takes down the MCP server. That reasoning is sound and I have not changed the behaviour, but by the registry's own definition it is a real gap, so it seemed better written down than unlisted. Happy to reclassify if you read it differently.

Gates run

  • pnpm lint && pnpm typecheck && pnpm test:unit
  • pnpm test:e2e
  • pnpm gate:install
  • pnpm test:e2e:desktop
  • None of the above tiers apply to this change

This adds a field to an observed event, so test:e2e is the tier and I could not run it on this machine, so I have left the box unticked; CI ran e2e, desktop-e2e and verify on this branch and all passed.

Two unit tests fail on my machine on a clean main as well: the @reticlehq/next loader and the @reticlehq/vite-plugin CJS require probe, both 5s timeouts under parallel load on Windows, plus formatGeneratedSource's Prettier sync resolution.

Checklist

  • Every commit is signed off
  • Tests added (RED then GREEN)
  • No any, no free strings, no non-null !
  • No console.log or internal tracking codes
  • Each changed file is under the 1000-line cap
  • CHANGELOG.md updated under [Unreleased]
  • Not security-affecting beyond making an existing capture honest about its own bound

Heads up on ordering: this touches packages/server/src/input/network-detail.ts, the same file as the NET_DETAIL URL redaction PR I opened alongside it. They are independent changes to different functions and both apply cleanly to main on their own, but whichever lands second will want a trivial rebase. Happy to do that, in either order, or to fold them into one PR if you would rather review them together.

thegoodengineer and others added 3 commits September 6, 2026 10:29
Rule 12 says a transform that truncates data on a path an agent reads must
report that it did. `buildNetworkDetail` caps the request body it takes off
the network stack at 8,192 characters and reports nothing.

The in-page observer caps the same body and does report it: `projectBody`
returns `{ body, truncated }` and the NET_REQUEST carries
`requestBodyTruncated`. So the two capture routes disagreed about whether a
shortened body announces itself, and the driven route is the one whose body
OVERWRITES the other in the merge, deliberately, because when they disagree
the disagreement is the finding.

That makes it concrete. A request whose page body was complete, merged with a
wire body that was cut, produced an event that read as a whole body and was
not, and an agent asserting over that payload reads the missing tail as an
absence. The reverse leaves a stale `requestBodyTruncated: true` caveating a
body that is now whole.

The bound is right and is untouched. Only the silence:

  - `projectWireBody` returns `{ body, truncated }`, the shape
    `sanitizeWithReport` set as the reference.
  - `buildNetworkDetail` emits `requestBodyTruncated: true`, the same field
    name the in-page path already uses, omitted when the whole body fits so
    the caveat means something when it appears.
  - The merge carries the flag with the body that won, and deletes it when the
    body that replaced a truncated one is whole.

`packages/server/src/input/network-detail.ts` is now registered in
`scripts/check-lossy-transforms.mjs` with every export classified, since the
guard catches an unclassified export in a registered module and this module
was never registered. `attachNetworkDetail` is classified `silent`: it drops a
response whose `headers()` rejects, which is the right call on the stdio path
where an unhandled rejection takes down the MCP server, but it is a real gap
by the registry's own definition and belongs written down rather than
unlisted.

Signed-off-by: Abhijeet Sharma <meetabhijeet05@gmail.com>
@divshekhar
divshekhar merged commit 6c3b761 into reticlehq:main Sep 6, 2026
19 checks passed
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.

2 participants