Files
scriptc/tests/corpus/fetch-headers-copy-cache.ts
T
Chris Tate 998a385bde Mark constructed headers canonical only when every value is trimmed
The Headers and Response constructors combine duplicate entries (a: x,
a: "") into one value with the separator, which can end in its space
("x, "). They marked the pairs canonical anyway, so a copy took them
unchanged where Node trims the copied value ("x,"). The constructors
now mark the pairs canonical only when every value is already trimmed;
otherwise copies normalize them pair by pair.
2026-10-23 10:45:26 -05:00

126 lines
4.7 KiB
TypeScript

// Headers copies and the cached sort-and-combine list: copies are
// independent of their source, reads see every mutation, and live
// iterators re-read the current list after a change.
function dump(label: string, h: Headers): void {
const parts: string[] = [];
h.forEach((value, name) => parts.push(`${name}=${value}`));
console.log(`${label}: ${parts.join(" | ")}`);
}
// The corpus lib has no DOM iterable typings for Headers, so iterators
// are reached through an untyped view.
const iter = (h: Headers): any => h;
const source = new Headers({ "X-B": " two ", "x-a": "one", Accept: "text/html" });
source.append("accept", "application/json");
source.append("Set-Cookie", "a=1");
source.append("set-cookie", "b=2");
dump("source", source);
// A Headers copy keeps the combined values and stays separate.
const copy = new Headers(source);
source.set("x-a", "changed");
copy.append("x-c", "three");
dump("source after set", source);
dump("copy after append", copy);
console.log(JSON.stringify(copy.get("accept")), JSON.stringify(copy.get("set-cookie")));
console.log(JSON.stringify(copy.getSetCookie()));
// Copying into an empty Response and a Request, then appending a repeat.
const response = new Response("body", { headers: copy, status: 201 });
response.headers.append("accept", "*/*");
response.headers.append("cookie", "c=3");
response.headers.append("Cookie", "d=4");
dump("response", response.headers);
dump("copy unchanged", copy);
const clone = new Response(response.body, response);
dump("response clone", clone.headers);
const request = new Request("http://example.test/", { headers: copy });
console.log("request x-b", JSON.stringify(request.headers.get("X-B")));
// Every read after a mutation sees it: get, has, iteration, spread.
const h = new Headers([["x-z", "z"], ["x-m", "m"]]);
console.log([...iter(h).entries()].map(([n, v]) => `${n}:${v}`).join(","));
h.delete("x-z");
console.log([...iter(h).keys()].join(","), h.has("x-z"));
h.delete("x-missing");
console.log([...iter(h).values()].join(","));
h.append("x-a", "first");
console.log([...iter(h).entries()].map(([n, v]) => `${n}:${v}`).join(","));
h.set("X-M", "again");
console.log(JSON.stringify(h.get("x-m")), [...iter(h).keys()].join(","));
// A live iterator: mutations between steps change what comes next.
const it = iter(h).entries();
console.log(JSON.stringify(it.next().value));
h.append("x-0", "before");
console.log(JSON.stringify(it.next().value));
h.delete("x-m");
h.append("x-zz", "after");
console.log(JSON.stringify(it.next()), JSON.stringify(it.next()));
// forEach that mutates as it walks.
const walk = new Headers([["b", "2"], ["d", "4"]]);
const seen: string[] = [];
walk.forEach((value, name) => {
seen.push(`${name}=${value}`);
if (name === "b") {
walk.append("c", "3");
walk.append("a", "1");
}
if (name === "c") walk.set("d", "four");
});
console.log(seen.join(" | "));
// Invalid input in a copy source is still rejected (plain object source).
try {
new Response("x", { headers: { "bad name": "v" } });
} catch (error) {
console.log((error as Error).name, (error as Error).message);
}
try {
new Headers(source).append("x", "a\nb");
} catch (error) {
console.log((error as Error).name, (error as Error).message);
}
// Appending an empty value leaves the separator's space at the end of the
// combined value; a copy trims it again, as Node's does. A leading
// separator has no whitespace to trim.
const spaced = new Headers();
spaced.append("a", "x");
spaced.append("a", "");
spaced.append("b", "");
spaced.append("b", "y");
spaced.append("c", "");
spaced.append("c", "");
spaced.append("cookie", "k=v");
spaced.append("cookie", "");
const spacedList = (headers: Headers): string => {
const out: string[] = [];
headers.forEach((value, name) => out.push(`${name}=${JSON.stringify(value)}`));
return out.join(" ");
};
console.log(spacedList(spaced));
console.log(spacedList(new Headers(spaced)));
console.log(spacedList(new Headers(new Headers(spaced))));
console.log(spacedList(new Response("", { headers: spaced }).headers));
console.log(spacedList(new Request("http://example.test/", { headers: spaced }).headers));
// The same through duplicate constructor entries, in a Headers object and
// in a Response's own headers.
const entries: [string, string][] = [
["a", "x"],
["a", ""],
["c", ""],
["c", ""],
["d", "plain"],
];
const built = new Headers(entries);
console.log(spacedList(built));
console.log(spacedList(new Headers(built)));
const builtResponse = new Response("", { headers: entries });
console.log(spacedList(builtResponse.headers));
console.log(spacedList(new Headers(builtResponse.headers)));
console.log(spacedList(new Request("http://example.test/", { headers: built }).headers));