fix(protobuf): preserve MIME admission and transport facts

Parse MIME parameters once with strict quoted-value and duplicate handling,
and compare equivalent default binary profiles. Keep excluded HAR MIME
unknown, explicit framing independent, and nominal gRPC sanitization intact
across malformed or repeated declarations.

Add public CLI/MCP and native-header regressions with source identity and
partial-coverage assertions. Preserve upstream wire and schema admission.

Fixes #6836
This commit is contained in:
morluto
2026-10-24 05:52:50 +08:00
parent b6e107aded
commit 0b1af48a37
5 changed files with 563 additions and 60 deletions
+8
View File
@@ -146,6 +146,14 @@ Automatic binary Protobuf admission validates reserved MIME parameters:
absent. Illegal profiles retain the source body, add a limitation, and leave
protobuf coverage partial. Ordinary non-encoding parameters remain accepted;
an explicit framing choice interprets bytes independently of the MIME profile.
Admission follows HTTP token and quoted-string syntax: unterminated quotes and
conflicting repeated reserved parameters remain invalid. Repeated headers with
implicit binary encoding and `encoding=binary` agree without rewriting either
header. Invalid declarations remain visible even with explicit framing, and
declared gRPC bodies still receive credential checks before a MIME refusal.
An excluded HAR `mimeType` remains unknown for automatic interpretation even
when a visible Content-Type header exists. That header can still identify the
transport for credential checks; explicit wire framing remains independent.
Native gRPC admits only data-frame flags 0/1; gRPC-web additionally admits its
terminal trailer frame. `grpc-web-text` supports independently padded base64
@@ -56,11 +56,15 @@ const captureWithBody = (
],
});
const decode = async (capture: WebNetworkCapture) => {
const decode = async (
capture: WebNetworkCapture,
framing: "auto" | "raw" = "auto",
) => {
const input = inspectWebNetworkCaptureInputSchema.parse({
capture_path: capture.artifact.path,
format: capture.format,
record_ordinals: [0],
protobuf: { framing },
});
const resources = new ArtifactResourceScope();
try {
@@ -128,6 +132,138 @@ it("keeps capture coverage partial for a malformed gRPC-web trailer", async () =
expect(report.records[0]?.binary_fields[2]).toEqual(retainedBody);
});
it.each([false, true])(
"admits equivalent default binary profiles in repeated byte headers (reverse=%s)",
async (reverse) => {
const types = [
"Application/Protobuf;type=left",
'application/protobuf;ENCODING="binary";type="right;=value"',
];
if (reverse) types.reverse();
const report = captureWithBody(types[0]!, Buffer.from([8, 1]));
const record = report.records[0]!;
record.reported = {
response: {
headers: [
[null, null],
[null, null],
],
content: null,
},
};
record.binary_fields.push(
binary("/response/headers/1/0", Buffer.from("Content-Type")),
binary("/response/headers/1/1", Buffer.from(types[1]!)),
);
const retained = structuredClone(record.binary_fields);
await decode(report);
expect(report.protobuf_coverage?.status).toBe("complete");
expect(record.protobuf?.[0]).toMatchObject({
content_type: types[0],
framing: "raw",
fields: [{ number: 1, value: "1" }],
});
expect(record.binary_fields).toEqual(retained);
},
);
it.each([false, true])(
"keeps incompatible repeated MIME headers unknown (reverse=%s)",
async (reverse) => {
const types = [
"application/protobuf",
"application/protobuf;encoding=json",
];
if (reverse) types.reverse();
const report = captureWithBody(types[0]!, Buffer.from([8, 1]));
const record = report.records[0]!;
record.reported = {
response: {
headers: [
[null, null],
[null, null],
],
content: null,
},
};
record.binary_fields.push(
binary("/response/headers/1/0", Buffer.from("Content-Type")),
binary("/response/headers/1/1", Buffer.from(types[1]!)),
);
const retained = structuredClone(record.binary_fields);
await decode(report);
expect(report.protobuf_coverage?.status).toBe("partial");
expect(record.protobuf).toBeUndefined();
expect(record.limitations).toContainEqual(
expect.stringContaining(
"MIME metadata is excluded, conflicting or unavailable",
),
);
expect(record.binary_fields).toEqual(retained);
},
);
it.each(["auto", "raw"] as const)(
"preserves declared gRPC credential exclusions after MIME refusal with %s framing",
async (framing) => {
const invalid = 'application/grpc-web;note="unterminated';
const valid = "application/grpc-web";
for (const types of [
[invalid],
[invalid, invalid],
[valid, invalid],
[invalid, valid],
]) {
const report = captureWithBody(
types[0]!,
grpcWebTrailerFrame(
Buffer.from("authorization: Bearer fixture-only\r\n"),
),
);
const record = report.records[0]!;
if (types.length === 2) {
record.reported = {
response: {
headers: [
[null, null],
[null, null],
],
content: null,
},
};
record.binary_fields.push(
binary("/response/headers/1/0", Buffer.from("Content-Type")),
binary("/response/headers/1/1", Buffer.from(types[1]!)),
);
}
const headers = structuredClone(
record.binary_fields.filter(({ pointer }) =>
pointer.includes("/headers/"),
),
);
await decode(report, framing);
expect(report.protobuf_coverage?.status, types.join(" / ")).toBe(
"partial",
);
expect(record.protobuf).toBeUndefined();
expect(record.binary_fields[2]).toMatchObject({
state: "redacted",
content_base64: null,
sha256: null,
});
expect(record.redactions).toContainEqual({
pointer: "/response/content",
reason: "transport-credential",
});
expect(
record.binary_fields.filter(({ pointer }) =>
pointer.includes("/headers/"),
),
).toEqual(headers);
}
},
);
it.each([
"application/protobuf; encoding=json",
"application/protobuf; encoding=unknown",
+140 -58
View File
@@ -1,4 +1,3 @@
import { MIMEType } from "node:util";
import { isAbsolute } from "node:path";
import { CaptureFormatError } from "../CaptureFormatError.js";
import {
@@ -24,6 +23,7 @@ import {
type WebNetworkCaptureRecord,
} from "../../../domain/webNetworkCapture.js";
import type { JsonValue } from "../../../domain/jsonValue.js";
import { err, ok, type Result } from "../../../domain/result.js";
import { ProtobufBudget } from "./ProtobufWire.js";
import type { ProtobufSchema } from "./ProtobufSchema.js";
import {
@@ -94,39 +94,91 @@ const excludeBody = (
record.redactions.push({ pointer, reason });
};
/** Admit reserved binary-Protobuf parameters before selecting wire framing. */
const protobufMediaType = (value: string | undefined) => {
if (value === undefined)
return { framing: undefined, issue: undefined, key: undefined };
let mime: MIMEType;
try {
mime = new MIMEType(value);
} catch {
return {
framing: undefined,
issue:
"Selected body has an invalid MIME declaration; automatic protobuf applicability is unknown.",
key: undefined,
};
type ProtobufMimeAdmission = Result<
{
readonly framing: ProtobufDecoding["framing"] | undefined;
readonly key: string | undefined;
},
{
readonly _tag: "InvalidProtobufMime";
readonly issue: string;
readonly declaredFraming: ProtobufDecoding["framing"] | undefined;
}
const framing = Object.hasOwn(PROTOBUF_FRAMING, mime.essence)
? PROTOBUF_FRAMING[mime.essence as keyof typeof PROTOBUF_FRAMING]
: undefined;
const encoding = mime.params.get("encoding");
const charset = mime.params.get("charset");
const version = mime.params.get("version");
const invalid =
>;
const mediaType =
/^[\t ]*([!#$%&'*+.^_`|~0-9A-Za-z-]+)\/([!#$%&'*+.^_`|~0-9A-Za-z-]+)/;
// RFC 9110 token / quoted-string, including quoted-pair and obs-text.
const mimeParameter =
/([!#$%&'*+.^_`|~0-9A-Za-z-]+)=([!#$%&'*+.^_`|~0-9A-Za-z-]+|"(?:[\t \x21\x23-\x5b\x5d-\x7e\x80-\xff]|\\[\t \x21-\x7e\x80-\xff])*")/y;
/** Strict admission keeps malformed/duplicate parameters visible instead of repairing them. */
const protobufMediaType = (
value: string | undefined,
): ProtobufMimeAdmission => {
if (value === undefined) return ok({ framing: undefined, key: undefined });
const type = mediaType.exec(value);
const essence =
type === null
? undefined
: `${type[1]!.toLowerCase()}/${type[2]!.toLowerCase()}`;
const framing =
essence !== undefined && Object.hasOwn(PROTOBUF_FRAMING, essence)
? PROTOBUF_FRAMING[essence as keyof typeof PROTOBUF_FRAMING]
: undefined;
const invalid = (reason: string): ProtobufMimeAdmission =>
err({
_tag: "InvalidProtobufMime",
issue: `Selected body has an invalid MIME declaration (${reason}); automatic protobuf applicability is unknown.`,
declaredFraming: framing,
});
if (type === null) return invalid("malformed media type");
const parameters = new Map<string, string>();
let at = type[0].length;
while (at < value.length) {
while (value[at] === " " || value[at] === "\t") at++;
if (at === value.length) break;
if (value[at++] !== ";") return invalid("malformed media-type parameters");
while (value[at] === " " || value[at] === "\t") at++;
// RFC 9110 permits empty parameter slots.
if (at === value.length || value[at] === ";") continue;
mimeParameter.lastIndex = at;
const match = mimeParameter.exec(value);
if (match === null) return invalid("malformed media-type parameters");
at = mimeParameter.lastIndex;
const name = match[1]!.toLowerCase();
if (!["encoding", "charset", "version"].includes(name)) continue;
const raw = match[2]!;
const decoded = raw.startsWith('"')
? raw.slice(1, -1).replace(/\\([\s\S])/g, "$1")
: raw;
if (parameters.has(name) && parameters.get(name) !== decoded)
return invalid(`conflicting ${name} parameters`);
parameters.set(name, decoded);
}
const encoding = parameters.get("encoding");
const charset = parameters.get("charset");
const version = parameters.get("version");
if (
framing === "raw" &&
((encoding !== null && encoding !== "binary") ||
charset !== null ||
version !== null);
return {
framing: invalid ? undefined : framing,
issue: invalid
? "Selected binary Protobuf MIME profile has an illegal encoding, charset or reserved version parameter; retained bytes were not automatically interpreted. Select explicit framing to interpret the bytes independently of that declaration."
: undefined,
key: JSON.stringify([mime.essence, encoding, charset, version]),
};
((encoding !== undefined && encoding !== "binary") ||
charset !== undefined ||
version !== undefined)
)
return err({
_tag: "InvalidProtobufMime",
issue:
"Selected binary Protobuf MIME profile has an illegal encoding, charset or reserved version parameter; retained bytes were not automatically interpreted. Select explicit framing to interpret the bytes independently of that declaration.",
declaredFraming: framing,
});
return ok({
framing,
key: JSON.stringify([
essence,
framing === "raw" ? (encoding ?? "binary") : (encoding ?? null),
charset ?? null,
version ?? null,
]),
});
};
/** Format-aware owner of body metadata; callers never reinterpret producer JSON. */
@@ -155,6 +207,8 @@ const bodyMetadata = (
const message = object(object(record.reported)?.[side]);
if (message === undefined) return undefined;
const headers = new Map<string, string | undefined>();
let headerProfile = protobufMediaType(undefined);
let headerDeclaredFraming: ProtobufDecoding["framing"] | undefined;
let headerNamesKnown = Array.isArray(message.headers);
if (Array.isArray(message.headers)) {
for (const [index, item] of message.headers.entries()) {
@@ -180,17 +234,38 @@ const bodyMetadata = (
const key = name.toLowerCase();
const knownValue = typeof value === "string" ? value : undefined;
const previous = headers.get(key);
// Repeated declarations agree only when known and equal; media types by essence.
if (key === "content-type") {
const nextProfile = protobufMediaType(knownValue);
const nextDeclaredFraming = nextProfile.ok
? nextProfile.value.framing
: nextProfile.error.declaredFraming;
const agrees =
previous !== undefined &&
knownValue !== undefined &&
headerProfile.ok &&
nextProfile.ok &&
headerProfile.value.key === nextProfile.value.key;
const first = !headers.has(key);
headerDeclaredFraming =
first || headerDeclaredFraming === nextDeclaredFraming
? nextDeclaredFraming
: undefined;
headers.set(key, first ? knownValue : agrees ? previous : undefined);
headerProfile = first
? nextProfile
: agrees
? headerProfile
: protobufMediaType(undefined);
continue;
}
// Other repeated declarations agree only when known and equal.
headers.set(
key,
!headers.has(key)
? knownValue
: previous !== undefined &&
knownValue !== undefined &&
(key === "content-type"
? protobufMediaType(previous).key ===
protobufMediaType(knownValue).key
: previous === knownValue)
previous === knownValue
? previous
: undefined,
);
@@ -209,12 +284,19 @@ const bodyMetadata = (
const mimeType = object(
message[side === "request" ? "postData" : "content"],
)?.mimeType;
const contentType = typeof mimeType === "string" ? mimeType : header;
const contentType = typeof mimeType === "string" ? mimeType : undefined;
const profile = protobufMediaType(contentType);
// An independent header still identifies transport sanitization, not the
// excluded producer MIME field's automatic interpretation.
return {
contentType,
automaticFraming: profile.framing,
mimeProfileIssue: profile.issue,
mimeProfile: profile,
declaredFraming:
contentType === undefined
? headerDeclaredFraming
: profile.ok
? profile.value.framing
: profile.error.declaredFraming,
contentTypeKnown: contentType !== undefined,
httpIdentityKnown: true,
httpCodingKnown: true,
@@ -222,11 +304,10 @@ const bodyMetadata = (
}
if (pointer !== `/${side}/body` && pointer !== `/${side}/content`)
return undefined;
const profile = protobufMediaType(header);
return {
contentType: header,
automaticFraming: profile.framing,
mimeProfileIssue: profile.issue,
mimeProfile: headerProfile,
declaredFraming: headerDeclaredFraming,
contentTypeKnown:
header !== undefined ||
(headerNamesKnown && !headers.has("content-type")),
@@ -314,26 +395,25 @@ export const prepareCaptureProtobuf = async (
incomplete = true;
continue;
}
const { contentType, automaticFraming: automatic } = metadata;
const { contentType, mimeProfile, declaredFraming } = metadata;
const automatic = mimeProfile.ok ? mimeProfile.value.framing : undefined;
const automaticSelected =
options?.framing === undefined || options.framing === "auto";
if (automaticSelected && metadata.mimeProfileIssue !== undefined) {
if (!mimeProfile.ok) {
if (automaticSelected) incomplete = true;
record.limitations.push(mimeProfile.error.issue);
}
if (automaticSelected && !metadata.contentTypeKnown && field.bytes > 0) {
incomplete = true;
record.limitations.push(metadata.mimeProfileIssue);
continue;
record.limitations.push(
"Selected body's MIME metadata is excluded, conflicting or unavailable, so automatic protobuf framing applicability is unknown; its retained bytes were not interpreted. Select explicit framing to decode them.",
);
}
const framing =
options?.framing === undefined || options.framing === "auto"
? automatic
? (automatic ?? declaredFraming)
: options.framing;
if (framing === undefined) {
// An empty body has no protobuf content under any framing.
if (!metadata.contentTypeKnown && field.bytes > 0) {
incomplete = true;
record.limitations.push(
"Selected body's MIME metadata is excluded, conflicting or unavailable, so automatic protobuf framing applicability is unknown; its retained bytes were not interpreted. Select explicit framing to decode them.",
);
}
continue;
}
if (
@@ -374,9 +454,9 @@ export const prepareCaptureProtobuf = async (
// Caller overrides do not turn off exclusions for the transport declared by the source.
const declared =
prepared.excluded === undefined &&
automatic !== undefined &&
automatic !== framing
? await prepareProtobufBody(body, automatic, exclusions, signal)
declaredFraming !== undefined &&
declaredFraming !== framing
? await prepareProtobufBody(body, declaredFraming, exclusions, signal)
: prepared;
const excluded = prepared.excluded ?? declared.excluded;
if (excluded !== undefined) {
@@ -407,6 +487,8 @@ export const prepareCaptureProtobuf = async (
);
continue;
}
if (automaticSelected && (!mimeProfile.ok || !metadata.contentTypeKnown))
continue;
bodies.push({ field, body, contentType, framing, prepared });
}
}
@@ -0,0 +1,273 @@
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { webNetworkCaptureSchema } from "../../../src/domain/webNetworkCapture.js";
import { protobufFrame, protobufHar } from "../../fixtures/protobufCapture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const body = Buffer.from([8, 1]);
const bodySha = createHash("sha256").update(body).digest("hex");
const hiddenMime = "application/json;type=excluded-declaration";
const automaticProfiles = [
['application/protobuf;encoding="binary', "malformed media-type parameters"],
[
"application/protobuf;encoding=binary;encoding=json",
"conflicting encoding parameters",
],
['Application/Protobuf;ENCODING="b\\inary";type="name;=value"', undefined],
] as const;
cliTest(
"admits complete MIME facts without repairing malformed protobuf parameters through CLI/MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-protobuf-mime-admission-");
const call = await cli.sharedMcpCall!();
for (const [index, [mime, problem]] of automaticProfiles.entries()) {
const path = join(root, `${index}.har`);
const original = protobufHar(body, mime);
await writeFile(path, original);
const output = await cli.sharedCliRun!({
arguments: [
"inspect-web-network-capture",
path,
"har",
"--record",
"0",
"--json",
],
});
expect(output.exitCode, output.stdout + output.stderr).toBe(0);
const evidence = parseEvidence(output.json);
const report = webNetworkCaptureSchema.parse(evidence.normalized_result);
expect(report.artifact).toMatchObject({
path,
sha256: createHash("sha256").update(original).digest("hex"),
});
expect(report.protobuf_coverage).toEqual({
status: problem === undefined ? "complete" : "partial",
selected_records: 1,
inspected_records: 1,
});
if (problem === undefined)
expect(report.records[0]?.protobuf?.[0]).toMatchObject({
content_type: mime,
framing: "raw",
status: "complete",
fields: [{ number: 1, value: "1" }],
});
else {
expect(report.records[0]?.protobuf).toBeUndefined();
expect(report.records[0]?.limitations).toContainEqual(
expect.stringContaining(problem),
);
}
expect(report.records[0]?.reported).toMatchObject({
response: {
content: { mimeType: mime, text: body.toString("base64") },
},
});
expect(report.records[0]?.binary_fields).toContainEqual(
expect.objectContaining({
pointer: "/response/content/text",
state: "retained",
content_base64: body.toString("base64"),
bytes: 2,
sha256: bodySha,
}),
);
const reply = await call("inspect_web_network_capture", {
capture_path: path,
format: "har",
record_ordinals: [0],
});
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
expect(parseEvidence(reply.structuredContent).evidence_id).toBe(
evidence.evidence_id,
);
expect(await readFile(path, "utf8")).toBe(original);
}
},
);
cliTest(
"keeps an excluded HAR MIME unknown beside a visible contradictory header through CLI/MCP",
async ({ cli }) => {
const root = await createTestTempDirectory(
"rea-protobuf-har-mime-unknown-",
);
const path = join(root, "capture.har");
const original = protobufHar(
body,
hiddenMime,
[],
[{ name: "Content-Type", value: "application/protobuf" }],
);
await writeFile(path, original);
const call = await cli.sharedMcpCall!();
for (const framing of ["auto", "raw"] as const) {
const output = await cli.sharedCliRun!({
arguments: [
"inspect-web-network-capture",
path,
"har",
"--record",
"0",
"--protobuf-framing",
framing,
"--sensitive-value",
"excluded-declaration",
"--json",
],
});
expect(output.exitCode, output.stdout + output.stderr).toBe(0);
const evidence = parseEvidence(output.json);
const report = webNetworkCaptureSchema.parse(evidence.normalized_result);
expect(report.records[0]?.reported).toMatchObject({
response: {
headers: [{ name: "Content-Type", value: "application/protobuf" }],
content: { mimeType: null },
},
});
expect(report.records[0]?.binary_fields).toContainEqual(
expect.objectContaining({
state: "retained",
bytes: 2,
sha256: bodySha,
}),
);
if (framing === "auto") {
expect(report.protobuf_coverage?.status).toBe("partial");
expect(report.records[0]?.protobuf).toBeUndefined();
expect(report.records[0]?.limitations).toContainEqual(
expect.stringContaining(
"MIME metadata is excluded, conflicting or unavailable",
),
);
} else {
expect(report.protobuf_coverage?.status).toBe("complete");
expect(report.records[0]?.protobuf?.[0]).toMatchObject({
content_type: null,
framing: "raw",
fields: [{ number: 1, value: "1" }],
});
}
const reply = await call("inspect_web_network_capture", {
capture_path: path,
format: "har",
record_ordinals: [0],
sensitive_values: ["excluded-declaration"],
protobuf: { framing },
});
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
expect(parseEvidence(reply.structuredContent).evidence_id).toBe(
evidence.evidence_id,
);
}
expect(await readFile(path, "utf8")).toBe(original);
},
);
cliTest(
"sanitizes visible HAR gRPC transport while its authoritative MIME is excluded through MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-protobuf-har-transport-");
const path = join(root, "capture.har");
const trailer = protobufFrame(
Buffer.from("authorization: Bearer fixture-only\r\n"),
128,
);
const original = protobufHar(
trailer,
hiddenMime,
[],
[{ name: "Content-Type", value: "application/grpc-web" }],
);
await writeFile(path, original);
const call = await cli.sharedMcpCall!();
for (const framing of ["auto", "raw"] as const) {
const reply = await call("inspect_web_network_capture", {
capture_path: path,
format: "har",
record_ordinals: [0],
sensitive_values: ["excluded-declaration"],
protobuf: { framing },
});
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
const report = webNetworkCaptureSchema.parse(
parseEvidence(reply.structuredContent).normalized_result,
);
expect(report.protobuf_coverage?.status).toBe("partial");
expect(report.records[0]?.protobuf).toBeUndefined();
expect(report.records[0]?.reported).toMatchObject({
response: {
headers: [{ name: "Content-Type", value: "application/grpc-web" }],
content: { mimeType: null, text: null },
},
});
expect(report.records[0]?.binary_fields).toContainEqual(
expect.objectContaining({
pointer: "/response/content/text",
state: "redacted",
content_base64: null,
sha256: null,
}),
);
expect(report.records[0]?.redactions).toContainEqual({
pointer: "/response/content/text",
reason: "transport-credential",
});
}
expect(await readFile(path, "utf8")).toBe(original);
},
);
cliTest(
"retains caller-selected raw interpretation and the invalid recorded MIME through CLI/MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-protobuf-mime-explicit-");
const path = join(root, "capture.har");
const mime = 'application/protobuf;encoding="binary';
const original = protobufHar(body, mime);
await writeFile(path, original);
const output = await cli.sharedCliRun!({
arguments: [
"inspect-web-network-capture",
path,
"har",
"--record",
"0",
"--protobuf-framing",
"raw",
"--json",
],
});
expect(output.exitCode, output.stdout + output.stderr).toBe(0);
const evidence = parseEvidence(output.json);
const report = webNetworkCaptureSchema.parse(evidence.normalized_result);
expect(report.protobuf_coverage?.status).toBe("complete");
expect(report.records[0]?.protobuf?.[0]).toMatchObject({
content_type: mime,
framing: "raw",
fields: [{ number: 1, value: "1" }],
});
expect(report.records[0]?.limitations).toContainEqual(
expect.stringContaining("invalid MIME declaration"),
);
const call = await cli.sharedMcpCall!();
const reply = await call("inspect_web_network_capture", {
capture_path: path,
format: "har",
record_ordinals: [0],
protobuf: { framing: "raw" },
});
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
expect(parseEvidence(reply.structuredContent).evidence_id).toBe(
evidence.evidence_id,
);
expect(await readFile(path, "utf8")).toBe(original);
},
);
+5 -1
View File
@@ -42,6 +42,10 @@ export const protobufHar = (
body: Buffer = protobufCaptureBody(),
mimeType = "application/grpc-web+proto",
additionalBodies: readonly Buffer[] = [],
responseHeaders: readonly {
readonly name: string;
readonly value: string;
}[] = [],
) =>
JSON.stringify({
log: {
@@ -65,7 +69,7 @@ export const protobufHar = (
statusText: "OK",
httpVersion: "HTTP/1.1",
cookies: [],
headers: [],
headers: responseHeaders,
redirectURL: "",
headersSize: -1,
bodySize: body.length,