Skip to content

stdlib: for…of headers.keys() produces empty iteration; headers.forEach works #576

@proggeramlug

Description

@proggeramlug

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions