oven-sh/bun · error · Error

generate-string-map: ${name}: keys ${JSON.stringify(k)} coll

Error message

generate-string-map: ${name}: keys ${JSON.stringify(k)} collide under case folding

What it means

With `emitCaseInsensitive: true`, the generator lowercases every key and keeps a `seenCi` set. Two distinct keys that fold to the same lowercase form ("Content-Type" and "content-type") would be indistinguishable to the generated `<name>_ignore_ascii_case` match, so the generator aborts instead of emitting ambiguous arms.

Source

Thrown at src/codegen/generate-string-map.ts:234

function buildOne<V>(spec: StringMapSpec<V>): string {
  const { name, valueTy, entries, emitKeys, emitIndexOf, emitCaseInsensitive, doc } = spec;
  const emitValue = spec.emitValue ?? ((v: V) => (typeof v === "string" ? JSON.stringify(v) : String(v)));

  const seen = new Set<string>();
  const seenCi = new Set<string>();
  for (const [k] of entries) {
    if (seen.has(k)) throw new Error(`generate-string-map: duplicate key ${JSON.stringify(k)} in ${name}`);
    seen.add(k);
    if (emitCaseInsensitive) {
      // eslint-disable-next-line no-control-regex
      if (!/^[\x00-\x7f]*$/.test(k)) {
        throw new Error(
          `generate-string-map: ${name}: emitCaseInsensitive requires ASCII keys; ${JSON.stringify(k)} is not`,
        );
      }
      const lk = k.toLowerCase();
      if (seenCi.has(lk)) {
        throw new Error(`generate-string-map: ${name}: keys ${JSON.stringify(k)} collide under case folding`);
      }
      seenCi.add(lk);
    }
  }

  const kvs: Array<readonly [Buffer, string]> = entries.map(([k, v]) => [Buffer.from(k, "utf8"), emitValue(v)]);
  const out: string[] = [];
  if (doc) for (const line of doc.split("\n")) out.push(`/// ${line}`.trimEnd());
  emitLookup(out, name, valueTy, kvs, { ci: false });

  if (emitCaseInsensitive) {
    out.push(``);
    emitLookup(out, `${name}_ignore_ascii_case`, valueTy, kvs, { ci: true });
  }

  if (emitIndexOf) {
    // Index in *declaration order* — same as `<NAME>_KEYS` so
    // `KEYS[index_of(k).unwrap()]` round-trips. u16 is plenty (asserted).

View on GitHub (pinned to 8c5296ac45)

Solutions

  1. Delete one of the colliding entries (keep the canonical casing) — the case-insensitive lookup matches both anyway.
  2. Rename one key so the lowercase forms differ.
  3. Turn off `emitCaseInsensitive` if the two casings must map to different values.

Example fix

// before
emitCaseInsensitive: true,
entries: [
  ["Content-Type", A],
  ["content-type", B], // collides under case folding
],

// after
emitCaseInsensitive: true,
entries: [["Content-Type", A]],
Defensive patterns

Strategy: validation

Validate before calling

function assertNoCaseCollisions(name, entries, emitCaseInsensitive) {
  if (!emitCaseInsensitive) return;
  const seenCi = new Set();
  for (const [k] of entries) {
    const lk = k.toLowerCase();
    if (seenCi.has(lk)) throw new Error(`${name}: ${k} collides under case folding`);
    seenCi.add(lk);
  }
}

Prevention

When it happens

Trigger: A case-insensitive spec containing two entries whose keys differ only in ASCII letter case.

Common situations: Adding an all-lowercase HTTP header name next to its canonical mixed-case form; merging tables from two sources that used different casing conventions.

Related errors


AI-assisted analysis of oven-sh/bun@8c5296ac45 (2026-08-16). Data as JSON: /api/errors/8b5fc2704fb0305b. Report an issue: GitHub.