fix(browser): share decoded response emptiness across capture views

This commit is contained in:
morluto committed 2026-10-24 06:13:53 +08:00
1 parent 836cbd3244
commit 59022b6a3a
5 files changed
+70 -12

No files matched your search

+3 -1
View File
@@ -465,7 +465,9 @@ api-surface grouping, then keeps the groups on the primary origin, the origin
with the most JSON-like records (`application/json`, `application/*+json`,
`text/plain`, `text/markdown`), whose path starts with `/api/` or `/apis/` or
whose content types are JSON-like, and the write groups answered with no body
(a bodiless `204 DELETE`). Every other group is listed under
(a bodiless `204 DELETE`). Empty retained response text establishes a bodiless
write only when its encoding is understood and unredacted; an opaque or hidden
encoding leaves the body unknown. Every other group is listed under
`excluded` with reason `other-origin`, `ui-page-or-asset` or `non-json`, and
retained records without a usable request line are counted in a limitation.
@@ -11,6 +11,7 @@ import {
type WebNetworkCaptureRecord,
} from "../webNetworkCapture.js";
import { completeWithinViewCoverage } from "./analysisViewCoverage.js";
import { recordedResponseBodyIsEmpty } from "./recordedResponseBody.js";
import type { UnsignedAnalysisView } from "./analysisView.js";
import { WebNetworkProjectionAccumulator } from "./WebNetworkProjectionAccumulator.js";
import {
@@ -105,14 +106,14 @@ const responseTextSchema = z.object({
}),
});
/** Whether a record's response had no body: a 204 or 205, an empty text, or a recorded size of zero; a missing text alone may be a body not kept. */
const emptyResponse = (reported: unknown): boolean => {
const parsed = responseTextSchema.safeParse(reported);
const emptyResponse = (record: WebNetworkCaptureRecord): boolean => {
const parsed = responseTextSchema.safeParse(record.reported);
if (!parsed.success) return false;
const { status, content } = parsed.data.response;
return (
status === 204 ||
status === 205 ||
content?.text === "" ||
recordedResponseBodyIsEmpty(record) ||
((content?.text === undefined || content.text === null) &&
content?.size === 0)
);
@@ -292,7 +293,7 @@ export class EnvironmentManifestAccumulator extends WebNetworkProjectionAccumula
route,
queryNames,
{ key, group },
{ shape: bodyShape.value, empty: emptyResponse(record.reported) },
{ shape: bodyShape.value, empty: emptyResponse(record) },
);
} finally {
this.releaseTemporary();
@@ -30,6 +30,7 @@ import {
import {
base64PrefixChars,
readRecordedResponseBody,
recordedResponseBodyIsEmpty,
type RecordedResponseBody,
} from "./recordedResponseBody.js";
import {
@@ -296,13 +297,7 @@ export class EnvironmentStateAccumulator extends EnvironmentManifestAccumulator
const entry = this.#entries.get(ordinal);
if (entry === undefined) return false;
if (entry.facts.status === 204) return true;
// Emptiness needs only a tiny decoded prefix; never decode a whole body outside the state budget.
const body = readRecordedResponseBody(entry.record, 0);
return (
body.bodyText === "" &&
body.bodyEncoding === undefined &&
body.bodyCut !== true
);
return recordedResponseBodyIsEmpty(entry.record);
}
/** Project the init state of the manifest's primary origin; records keep timeline order. */
@@ -1102,6 +1102,54 @@ it("keeps a write answered with no body as a tool whatever its content type, but
]);
});
it("keeps opaque or redacted empty response text unknown when selecting bodiless writes", () => {
const capture = syntheticCapture([
["POST", "https://s.test/opaque/1", "text/html"],
["POST", "https://s.test/hidden/1", "text/html"],
["POST", "https://s.test/base64/1", "text/html"],
]);
for (const [index, record] of capture.records.entries()) {
record.reported = {
...(record.reported as Record<string, unknown>),
response: {
status: 200,
content: {
mimeType: "text/html",
text: "",
...(index === 0 ? { encoding: "opaque" } : {}),
...(index === 2 ? { encoding: "base64" } : {}),
},
},
};
if (index === 1)
record.redactions = [
{
pointer: "/response/content",
reason: "explicit-sensitive-value",
scope: "property-name",
},
];
if (index === 2)
record.binary_fields = [
{
pointer: "/response/content/text",
representation: "har-base64-content",
state: "retained",
content_base64: "",
bytes: 0,
sha256:
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
];
}
const projected = manifest([], capture);
expect(projected.tools.map(line)).toEqual(["POST /base64/{numeric}"]);
expect(projected.excluded.map((entry) => entry.path_template)).toEqual([
"/hidden/{numeric}",
"/opaque/{numeric}",
]);
});
it("refuses a parent whose subject is absent or names another path or digest", () => {
const parent = parentOf();
const refusal = {
@@ -70,3 +70,15 @@ export const readRecordedResponseBody = (
return { bodyText: encoded, bodyEncoding: "base64", ...flags };
}
};
/** Prove decoded emptiness with a bounded prefix, preserving encoding and cut uncertainty. */
export const recordedResponseBodyIsEmpty = (
record: WebNetworkCaptureRecord,
): boolean => {
const body = readRecordedResponseBody(record, 0);
return (
body.bodyText === "" &&
body.bodyEncoding === undefined &&
body.bodyCut !== true
);
};