Repro
const h = new Headers();
h.set("host", "s3.example.com");
h.set("x-amz-date", "20260508T072902Z");
h.set("x-amz-content-sha256", "abc");
const keys: string[] = [];
for (const k of h.keys()) {
keys.push(k);
}
console.log("keys via for…of:", keys);
const fkeys: string[] = [];
h.forEach((_v, k) => { fkeys.push(k); });
console.log("keys via forEach:", fkeys);
bun main.ts:
keys via for…of: [ "host", "x-amz-content-sha256", "x-amz-date" ]
keys via forEach: [ "host", "x-amz-content-sha256", "x-amz-date" ]
perry main.ts:
keys via for…of: []
keys via forEach: [ 'host', 'x-amz-date', 'x-amz-content-sha256' ]
forEach correctly yields every header. for…of headers.keys() yields nothing. So the underlying header storage is intact; the gap is specifically how .keys() interacts with for…of (i.e. Symbol.iterator).
Same bug likely affects for (const v of h.values()) and for (const [k, v] of h.entries()) — not separately verified but should be checked together.
Why this matters
Discovered while running @bradenmacdonald/s3-lite-client post #572/#573/#574 fix (#551). AWS SigV4's getHeadersToSign(headers) does:
function getHeadersToSign(headers: Headers): string[] {
const headersToSign = [];
for (const key of headers.keys()) {
if (ignoredHeaders.includes(key.toLowerCase())) continue;
headersToSign.push(key);
}
headersToSign.sort();
return headersToSign;
}
Returns an empty array → SignedHeaders is empty → the canonical request has no headers → the resulting signature is wrong. Every AWS-signed call produces an unauthenticated URL.
This also breaks anything that inspects request headers via the standard Headers iteration protocol — middleware loggers, request transformers, fetch interceptors, mocking frameworks.
Suggested implementation
Headers.keys() (and .values() / .entries()) need to return a real iterator that exposes Symbol.iterator returning itself. The for…of loop calls obj[Symbol.iterator]() on the iterator; if keys() returns an array-like or a tagged value that doesn't have that, the loop bails immediately.
Probably worth checking: the iterator's .next() shape, done/value flags, plus the Symbol.iterator-returns-self contract.
Acceptance
The repro at the top prints identical arrays for for…of and forEach. Plus regressions:
Array.from(h.keys()) — works (uses Symbol.iterator)
[...h.keys()] — works (spread also uses Symbol.iterator)
for (const v of h.values())
for (const [k, v] of h.entries())
for (const [k, v] of h) — direct iteration of Headers itself
Refs #551
Repro
bun main.ts:perry main.ts:forEachcorrectly yields every header.for…of headers.keys()yields nothing. So the underlying header storage is intact; the gap is specifically how.keys()interacts withfor…of(i.e.Symbol.iterator).Same bug likely affects
for (const v of h.values())andfor (const [k, v] of h.entries())— not separately verified but should be checked together.Why this matters
Discovered while running
@bradenmacdonald/s3-lite-clientpost #572/#573/#574 fix (#551). AWS SigV4'sgetHeadersToSign(headers)does:Returns an empty array → SignedHeaders is empty → the canonical request has no headers → the resulting signature is wrong. Every AWS-signed call produces an unauthenticated URL.
This also breaks anything that inspects request headers via the standard Headers iteration protocol — middleware loggers, request transformers, fetch interceptors, mocking frameworks.
Suggested implementation
Headers.keys()(and.values()/.entries()) need to return a real iterator that exposesSymbol.iteratorreturning itself. The for…of loop callsobj[Symbol.iterator]()on the iterator; ifkeys()returns an array-like or a tagged value that doesn't have that, the loop bails immediately.Probably worth checking: the iterator's
.next()shape,done/valueflags, plus theSymbol.iterator-returns-self contract.Acceptance
The repro at the top prints identical arrays for
for…ofandforEach. Plus regressions:Array.from(h.keys())— works (usesSymbol.iterator)[...h.keys()]— works (spread also usesSymbol.iterator)for (const v of h.values())for (const [k, v] of h.entries())for (const [k, v] of h)— direct iteration of Headers itselfRefs #551