fix(browser): preserve media type parameter spelling (#6985)

* fix(browser): preserve media type parameter spelling

* test(verifiers): follow shared command output and identity
This commit is contained in:
ShuaixinHuang authored and GitHub committed 2026-10-25 02:16:18 +08:00
1 parent 1ca7d38764
commit 85bc8c8977
2 files changed
+80 -2

No files matched your search

+65
View File
@@ -1,3 +1,4 @@
import { MIMEType } from "node:util";
import { describe, expect, it } from "vitest";
import { safeResponseMetadata } from "./CdpSafeMetadata.js";
@@ -69,6 +70,70 @@ describe("structured COOP and COEP declarations", () => {
);
});
describe("media type parameter preservation", () => {
it.each([
[
'Multipart/Mixed; boundary="AaB03x"',
'multipart/mixed; boundary="AaB03x"',
],
[
'Application/LD+JSON; profile="https://api.example/Profiles/Book;V=One,Two"',
'application/ld+json; profile="https://api.example/Profiles/Book;V=One,Two"',
],
[
'Application/X-Protobuf; type="Example.Message"',
'application/x-protobuf; type="Example.Message"',
],
])(
"preserves case-sensitive media type parameters in %s",
(raw, expected) => {
const linkType = raw.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
const captured = safeResponseMetadata(
"request-media-type",
`${origin}/page`,
{
mimeType: ` ${raw} `,
headers: {
"Content-Encoding": " GZip ",
Link: `</resource>; rel="PreLoad MCP"; type="${linkType}"; as=SCRIPT; crossorigin=ANONYMOUS`,
},
},
new Set([origin]),
);
for (const projected of [
captured.response.mime_type,
captured.response.links[0]?.type,
]) {
expect(projected).toBe(expected);
expect([...new MIMEType(projected ?? "").params]).toEqual([
...new MIMEType(raw).params,
]);
}
expect(captured.response.content_encoding).toBe("gzip");
expect(captured.response.links).toMatchObject([
{
href: `${origin}/resource`,
destination_scope: "approved",
context_url: `${origin}/page`,
context_scope: "approved",
rel: ["preload", "mcp"],
as: "script",
crossorigin: "anonymous",
},
]);
expect(captured.agentHints).toEqual([
{
mechanism: "link_rel",
declaration: "preload mcp",
url: `${origin}/resource`,
trust: "page-declared-untrusted",
},
]);
},
);
});
describe("safe CDP response metadata", () => {
it("drops credential headers and preserves complete local link URLs", () => {
const captured = safeResponseMetadata(
+15 -2
View File
@@ -53,7 +53,7 @@ export const safeResponseMetadata = (
response: {
request_id: requestId,
url,
mime_type: boundedHeader(cdpStringValue(response.mimeType)),
mime_type: mediaTypeDeclaration(cdpStringValue(response.mimeType)),
content_length: contentLength(field("content-length")),
content_encoding: boundedHeader(field("content-encoding")),
csp,
@@ -245,7 +245,9 @@ const parseLinks = (
context_scope: context.scope,
rel: rel.toLowerCase().split(/\s+/u).filter(Boolean),
as: boundedHeader(lastLinkParameter(parameters, "as")?.value),
type: boundedHeader(firstLinkParameter(parameters, "type")?.value),
type: mediaTypeDeclaration(
firstLinkParameter(parameters, "type")?.value,
),
crossorigin: boundedHeader(
lastLinkParameter(parameters, "crossorigin")?.value,
),
@@ -430,6 +432,17 @@ const contentLength = (value: string | undefined): number | null => {
const boundedHeader = (value: string | undefined): string | null =>
value === undefined ? null : value.trim().toLowerCase();
// RFC 9110 section 8.3.1: type/subtype are case-insensitive; parameters may not be.
const mediaTypeDeclaration = (value: string | undefined): string | null => {
if (value === undefined) return null;
const declaration = value.trim();
const separator = declaration.indexOf(";");
return separator < 0
? declaration.toLowerCase()
: declaration.slice(0, separator).toLowerCase() +
declaration.slice(separator);
};
const decodeLinkParameter = (value: string): string => {
if (!value.startsWith('"')) return value;
let decoded = "";