From 85bc8c8977a8103b57df9be923f268acebdcb617 Mon Sep 17 00:00:00 2001 From: ShuaixinHuang <1836656514@qq.com> Date: Sun, 25 Oct 2026 02:16:18 +0800 Subject: [PATCH] fix(browser): preserve media type parameter spelling (#6985) * fix(browser): preserve media type parameter spelling * test(verifiers): follow shared command output and identity --- src/browser/CdpSafeMetadata.test.ts | 65 +++++++++++++++++++++++++++++ src/browser/CdpSafeMetadata.ts | 17 +++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/browser/CdpSafeMetadata.test.ts b/src/browser/CdpSafeMetadata.test.ts index 036ecf0029..a9478c3aa9 100644 --- a/src/browser/CdpSafeMetadata.test.ts +++ b/src/browser/CdpSafeMetadata.test.ts @@ -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: `; 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( diff --git a/src/browser/CdpSafeMetadata.ts b/src/browser/CdpSafeMetadata.ts index 276e2689eb..1adcd87edd 100644 --- a/src/browser/CdpSafeMetadata.ts +++ b/src/browser/CdpSafeMetadata.ts @@ -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 = "";