FuelLabs/fuels-ts · error · FuelError

INVALID_INPUT_PARAMETERS

INVALID_INPUT_PARAMETERS

Error message

Invalid UTF-8 in the input string.

What it means

Thrown by toUtf8Bytes when, while encoding, a high-surrogate code unit (0xD800–0xDBFF) is either the last character in the string or is not followed by a low-surrogate (0xDC00–0xDFFF). UTF-16 requires surrogate pairs; an unpaired surrogate is invalid and cannot be encoded to UTF-8. The check fires after incrementing past the high surrogate.

Source

Thrown at packages/utils/src/utils/toUtf8Bytes.ts:33

    str = stri.normalize('NFC');
  }

  const result: Array<number> = [];

  for (let i = 0; i < str.length; i += 1) {
    const c = str.charCodeAt(i);

    if (c < 0x80) {
      result.push(c);
    } else if (c < 0x800) {
      result.push((c >> 6) | 0xc0);
      result.push((c & 0x3f) | 0x80);
    } else if ((c & 0xfc00) === 0xd800) {
      i += 1;
      const c2 = str.charCodeAt(i);

      if (i >= str.length || (c2 & 0xfc00) !== 0xdc00) {
        throw new FuelError(
          ErrorCode.INVALID_INPUT_PARAMETERS,
          'Invalid UTF-8 in the input string.'
        );
      }

      // Surrogate Pair
      const pair = 0x10000 + ((c & 0x03ff) << 10) + (c2 & 0x03ff);
      result.push((pair >> 18) | 0xf0);
      result.push(((pair >> 12) & 0x3f) | 0x80);
      result.push(((pair >> 6) & 0x3f) | 0x80);
      result.push((pair & 0x3f) | 0x80);
    } else {
      result.push((c >> 12) | 0xe0);
      result.push(((c >> 6) & 0x3f) | 0x80);
      result.push((c & 0x3f) | 0x80);
    }
  }

View on GitHub (pinned to b3f37c91ac)

Solutions

  1. Keep the default form=true so the input is NFC-normalized, which repairs lone surrogates before encoding.
  2. Sanitize input by calling str.normalize('NFC') before passing it.
  3. Avoid slicing/substring across surrogate pair boundaries; use code-point-aware utilities.
  4. Validate the string contains no lone surrogates with a regex check before encoding.

Example fix

// before
toUtf8Bytes('\uD800X', false); // throws: lone high surrogate

// after
toUtf8Bytes('\uD800X'.normalize('NFC')); // form defaults to true
// or sanitize:
const safe = str.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/g, '').replace(/(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, '');
toUtf8Bytes(safe, false);
Defensive patterns

Strategy: validation

Validate before calling

function hasLoneSurrogate(s: string): boolean {
  return /[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(s) ||
         /(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(s);
}
const safe = hasLoneSurrogate(str) ? str.normalize('NFC') : str;
toUtf8Bytes(safe);

Type guard

const isPairedSurrogates = (s: string): boolean =>
  !/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(s) &&
  !/(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(s);

Try / catch

try {
  return toUtf8Bytes(str, form);
} catch (e) {
  if (e instanceof FuelError && e.code === ErrorCode.INVALID_INPUT_PARAMETERS) {
    return toUtf8Bytes(str.normalize('NFC')); // retry with normalization
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling toUtf8Bytes(str, form) where form is false (NFC normalization disabled) and str contains a lone high surrogate — e.g. a string built with '\uD800' not followed by a low surrogate, or data sliced at a surrogate boundary. With form=true (default) str.normalize('NFC') usually prevents this.

Common situations: Disabling NFC normalization and feeding in text with broken surrogates (emoji split mid-pair, binary-as-text). Slicing a string in the middle of a surrogate pair. Constructing strings from char codes that produce unpaired surrogates. Reading non-UTF-8 bytes as a JS string.

Understand the failure class

Related errors


AI-assisted analysis of FuelLabs/fuels-ts@b3f37c91ac (2026-08-12). Data as JSON: /api/errors/424cd222a7807889. Report an issue: GitHub.