Merge pull request #21684 from calixteman/fix/quad-regex-headers

Trim the response headers with a backward scan
This commit is contained in:
Tim van der Meij 2026-08-02 12:46:03 +02:00 committed by GitHub
commit eb8f6af8cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 49 additions and 3 deletions

View File

@ -25,6 +25,7 @@ import {
ensureResponseOrigin,
extractFilenameFromHeader,
getResponseOrigin,
trimHeadersEnd,
validateRangeRequestCapabilities,
} from "./network_utils.js";
import { endRequests } from "./transport_stream.js";
@ -208,9 +209,7 @@ class PDFNetworkStreamReader extends BasePDFStreamReader {
const rawResponseHeaders = fullRequestXhr.getAllResponseHeaders();
const responseHeaders = new Headers(
rawResponseHeaders
? rawResponseHeaders
.trimStart()
.replace(/[^\S ]+$/, "") // Not `trimEnd`, to keep regular spaces.
? trimHeadersEnd(rawResponseHeaders.trimStart())
.split(/[\r\n]+/)
.map(x => {
const [key, ...val] = x.split(": ");

View File

@ -32,6 +32,17 @@ function createHeaders(isHttp, httpHeaders) {
return headers;
}
// Trim the trailing whitespace of the raw response headers, but keep the
// regular spaces (hence no `trimEnd`). Scanning backwards keeps this linear,
// whereas a `$`-anchored regex is quadratic in the length of the run.
function trimHeadersEnd(str) {
let end = str.length;
while (end > 0 && str[end - 1] !== " " && /\s/.test(str[end - 1])) {
end--;
}
return str.slice(0, end);
}
function getResponseOrigin(url) {
// Notably, null is distinct from "null" string (e.g. from file:-URLs).
return URL.parse(url)?.origin ?? null;
@ -117,5 +128,6 @@ export {
ensureResponseOrigin,
extractFilenameFromHeader,
getResponseOrigin,
trimHeadersEnd,
validateRangeRequestCapabilities,
};

View File

@ -17,6 +17,7 @@ import {
createHeaders,
createResponseError,
extractFilenameFromHeader,
trimHeadersEnd,
validateRangeRequestCapabilities,
} from "../../src/display/network_utils.js";
import { ResponseException } from "../../src/shared/util.js";
@ -386,4 +387,38 @@ describe("network_utils", function () {
testCreateResponseError(new URL("https://foo.com/bar.pdf"), 0, false);
});
});
describe("trimHeadersEnd", function () {
it("removes the trailing whitespace", function () {
expect(trimHeadersEnd("a: 1\r\nb: 2\r\n")).toEqual("a: 1\r\nb: 2");
expect(trimHeadersEnd("a: 1\n\n")).toEqual("a: 1");
expect(trimHeadersEnd("a: 1\t\r\n")).toEqual("a: 1");
});
it("keeps the regular spaces", function () {
expect(trimHeadersEnd("a: 1 ")).toEqual("a: 1 ");
expect(trimHeadersEnd("a: 1\r\n ")).toEqual("a: 1\r\n ");
expect(trimHeadersEnd(" ")).toEqual(" ");
});
it("handles strings without trailing whitespace", function () {
expect(trimHeadersEnd("")).toEqual("");
expect(trimHeadersEnd("a: 1")).toEqual("a: 1");
expect(trimHeadersEnd("\r\na: 1")).toEqual("\r\na: 1");
});
it("handles a long run of whitespace efficiently", function () {
// Removing the run with a `$`-anchored regex is quadratic in its length,
// and a server controls how long the headers are.
const run = "\t".repeat(100000);
const startTime = performance.now();
// The run is trailing, hence removed.
expect(trimHeadersEnd(`a: 1${run}`)).toEqual("a: 1");
// The run is followed by a non-whitespace, hence kept: this is the case
// which a regex has to backtrack over.
expect(trimHeadersEnd(`a: 1${run}b`)).toEqual(`a: 1${run}b`);
expect(performance.now() - startTime).toBeLessThan(1000);
});
});
});