test: close the empty-array hole in the e2e unstubbed-request guard (closes #187)
All checks were successful
check / check (push) Successful in 42s

batch.every() is vacuously true on an empty array, so a POST with body []
was fulfilled with 200 [] instead of being reported as an unstubbed
request. The one mechanism whose job is to fail the suite on unrecognised
outbound traffic let it through. Reject an empty batch explicitly; no real
JSON-RPC batch is empty, so nothing legitimate is caught by it.

Demonstrated by execution with a throwaway probe firing four POSTs the
harness does not stub. Before: [] fulfilled and its probe passed, while
the bodyless, scalar-JSON and [null] probes were reported and failed.
After: all four reported and failed, with tests 1-27 still green.

Also corrects the comment above the guard. postData() is
buffer.toString("utf-8") || null in playwright-core, so a binary body is
decoded lossily into invalid JSON and reported by the catch, not returned
as null; only an absent or empty body yields null and reaches the type
guard. Both paths report, but the stated reason was wrong.
This commit is contained in:
2026-08-12 09:41:57 +00:00
parent 5af89a1b63
commit 2048e7695f
2 changed files with 25 additions and 8 deletions

View File

@@ -297,17 +297,26 @@ async function handleRpc(route, postData, opts, report) {
// ethers batches by default, so the body may be an array.
const batch = Array.isArray(payload) ? payload : [payload];
// Anything that is not a JSON-RPC object, or a batch of them, is not
// RPC at all and must be reported like any other unrecognised
// outbound traffic rather than dereferenced. request.postData()
// returns null both for a bodyless POST and for a body Playwright
// cannot decode as UTF-8 (sendBeacon with a Blob, or any binary
// payload), so this is not an empty-string special case: it rejects
// every non-object payload, exactly as the catch above rejects every
// unparseable one.
// Anything that is not a JSON-RPC object, or a NON-EMPTY batch of
// them, is not RPC at all and must be reported like any other
// unrecognised outbound traffic rather than dereferenced.
//
// The length check is not decoration: every() is vacuously true on an
// empty array, so without it a POST with body [] was answered 200 []
// and escaped the guard entirely (issue #187). No real batch is empty,
// so nothing legitimate is caught by it.
//
// Two distinct paths land a non-RPC body here, and neither is an
// empty-string special case. playwright-core's postData() is
// `buffer.toString("utf-8") || null`, so an absent or empty body
// decodes to null, JSON.parse("null") yields null, and the type guard
// below reports it. A binary body is instead decoded LOSSILY into
// mojibake — not null — which is not valid JSON, so the catch above
// reports that one. Both end up reported; only the route differs.
if (
payload === null ||
typeof payload !== "object" ||
batch.length === 0 ||
!batch.every((req) => req !== null && typeof req === "object")
) {
report("unstubbed request: POST " + route.request().url());