colinhacks/zod · error · Error

Invalid hex string length

Error message

Invalid hex string length

What it means

Thrown by `util.hexToUint8Array` when the input string has an odd length after stripping a leading `0x` prefix (guard at util.ts:964). Hex bytes are pairs of characters, so an odd-length string cannot be parsed into bytes — the function rejects rather than guess padding. It's used internally by hash decoders and any consumer calling the util directly.

Source

Thrown at packages/zod/src/v4/core/util.ts:965

    binaryString += String.fromCharCode(bytes[i]);
  }
  return btoa(binaryString);
}

export function base64urlToUint8Array(base64url: string): InstanceType<typeof Uint8Array> {
  const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
  const padding = "=".repeat((4 - (base64.length % 4)) % 4);
  return base64ToUint8Array(base64 + padding);
}

export function uint8ArrayToBase64url(bytes: Uint8Array): string {
  return uint8ArrayToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}

export function hexToUint8Array(hex: string): InstanceType<typeof Uint8Array> {
  const cleanHex = hex.replace(/^0x/, "");
  if (cleanHex.length % 2 !== 0) {
    throw new Error("Invalid hex string length");
  }
  const bytes = new Uint8Array(cleanHex.length / 2);
  for (let i = 0; i < cleanHex.length; i += 2) {
    bytes[i / 2] = Number.parseInt(cleanHex.slice(i, i + 2), 16);
  }
  return bytes;
}

export function uint8ArrayToHex(bytes: Uint8Array): string {
  return Array.from(bytes)
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
}

// instanceof
export abstract class Class {
  constructor(..._args: any[]) {}
}

View on GitHub (pinned to 912f0f51b0)

Solutions

  1. Pre-validate length: `if (hex.replace(/^0x/, '').length % 2 !== 0) throw new Error('hex must be even-length')`.
  2. Pad with a leading zero if semantically correct: `hex.padStart(targetLen, '0')`.
  3. Regenerate the source string from `util.uint8ArrayToHex(bytes)` to guarantee even length.

Example fix

// before
const bytes = util.hexToUint8Array(rawHash); // throws if odd-length

// after
const clean = rawHash.replace(/^0x/, '');
const bytes = util.hexToUint8Array(clean.length % 2 ? '0' + clean : clean);
Defensive patterns

Strategy: validation

Validate before calling

function toBytes(hex: string) {
  const clean = hex.replace(/^0x/, '');
  if (clean.length % 2 !== 0) throw new Error('hex string must have even length');
  return util.hexToUint8Array(clean);
}

Type guard

function isEvenHex(s: string): boolean {
  return /^[0-9a-fA-F]*$/.test(s.replace(/^0x/, '')) &&
    s.replace(/^0x/, '').length % 2 === 0;
}

Try / catch

try { bytes = util.hexToUint8Array(raw); }
catch (e) {
  if (e instanceof Error && /hex string length/i.test(e.message)) {
    bytes = util.hexToUint8Array(raw.replace(/^0x/, '').padStart(/* even */, '0'));
  } else throw e;
}

Prevention

When it happens

Trigger: Calling `hexToUint8Array('abc')` (3 chars), `'0x1f3'` (3 after prefix), or any hex string with an odd character count. Also reached indirectly via `z.string().format('hash')` or custom code paths that decode hex hashes.

Common situations: Truncated or partially-trimmed hash strings from logs; copy-paste losing a character; concatenating hex fragments that joined to odd length; user input not pre-validated before being passed to a hash decoder.

Related errors


AI-assisted analysis of colinhacks/zod@912f0f51b0 (2026-08-03). Data as JSON: /data/errors/d3a2888ede6d9a5b.json. Report an issue: GitHub.