diff --git a/src/browser/CdpSafeMetadata.test.ts b/src/browser/CdpSafeMetadata.test.ts index 0cff4e094..ef9c42e14 100644 --- a/src/browser/CdpSafeMetadata.test.ts +++ b/src/browser/CdpSafeMetadata.test.ts @@ -289,4 +289,25 @@ describe("safe CDP response metadata referrer policy", () => { expect(captured.response.policies.referrer_policy).toBe(expected); expect(captured.response.policies.coop).toBe("same-origin"); }); + + it.each<[string | number | undefined, number | null]>([ + ["100", 100], + [" 0 ", 0], + [100, 100], + ["-5", null], + ["+12", null], + ["0x10", null], + ["1e3", null], + ["9007199254740991", 9007199254740991], + ["9007199254740992", null], + [undefined, null], + ])("declares Content-Length %j only for decimal digits", (raw, expected) => { + const captured = safeResponseMetadata( + "request-length", + `${origin}/api`, + { headers: raw === undefined ? {} : { "Content-Length": raw } }, + new Set([origin]), + ); + expect(captured.response.content_length).toBe(expected); + }); }); diff --git a/src/browser/CdpSafeMetadata.ts b/src/browser/CdpSafeMetadata.ts index 589c00367..1347cdcca 100644 --- a/src/browser/CdpSafeMetadata.ts +++ b/src/browser/CdpSafeMetadata.ts @@ -1,10 +1,6 @@ import type { WebPageInspection } from "../domain/browserObservationSchemas.js"; import { sanitizeBrowserUrl } from "../domain/browserObservation.js"; -import { - numberValue, - cdpStringValue, - type UnknownRecord, -} from "./CdpCaptureValues.js"; +import { cdpStringValue, type UnknownRecord } from "./CdpCaptureValues.js"; type ResponseMetadata = WebPageInspection["metadata"]["responses"][number]; type LinkMetadata = ResponseMetadata["links"][number]; @@ -43,7 +39,7 @@ export const safeResponseMetadata = ( request_id: requestId, url, mime_type: boundedHeader(cdpStringValue(response.mimeType)), - content_length: nonnegativeInteger(headers.get("content-length")), + content_length: contentLength(headers.get("content-length")), content_encoding: boundedHeader(headers.get("content-encoding")), csp, links, @@ -343,9 +339,12 @@ const policyToken = (value: string | undefined): string | null => { return /^[a-z][a-z0-9_.\x2d]*$/u.test(token) ? token : null; }; -const nonnegativeInteger = (value: string | undefined): number | null => { - const number = numberValue(value === undefined ? undefined : Number(value)); - return number === undefined ? null : Math.max(0, Math.trunc(number)); +// RFC 9110 section 8.6: Content-Length = 1*DIGIT. Other spellings declare no length. +const contentLength = (value: string | undefined): number | null => { + const token = (value ?? "").trim(); + if (!/^[0-9]+$/u.test(token)) return null; + const length = Number(token); + return Number.isSafeInteger(length) ? length : null; }; const boundedHeader = (value: string | undefined): string | null =>