denoland/deno · error · DOMException

InvalidCharacterError

InvalidCharacterError

Error message

Failed to decode base64: invalid character

What it means

atob forwards to the native op_base64_atob, which rejects any input outside the standard base64 alphabet (A-Z a-z 0-9 + / =) or with an impossible length; the resulting TypeError is converted to DOMException InvalidCharacterError 'Failed to decode base64: invalid character' (ext/web/05_base64.js:29-36). Note the fixed message: the specific bad character is not included. Common culprits are base64url tokens (with - and _), embedded whitespace/newlines, data: URI prefixes, and inputs whose length % 4 === 1.

Source

Thrown at ext/web/05_base64.js:32

  TypeErrorPrototype,
} = primordials;

const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js");
const { DOMException } = core.loadExtScript("ext:deno_web/01_dom_exception.js");

/**
 * @param {string} data
 * @returns {string}
 */
function atob(data) {
  const prefix = "Failed to execute 'atob'";
  webidl.requiredArguments(arguments.length, 1, prefix);
  data = webidl.converters.DOMString(data, prefix, "Argument 1");
  try {
    return op_base64_atob(data);
  } catch (e) {
    if (ObjectPrototypeIsPrototypeOf(TypeErrorPrototype, e)) {
      throw new DOMException(
        "Failed to decode base64: invalid character",
        "InvalidCharacterError",
      );
    }
    throw e;
  }
}

/**
 * @param {string} data
 * @returns {string}
 */
function btoa(data) {
  const prefix = "Failed to execute 'btoa'";
  webidl.requiredArguments(arguments.length, 1, prefix);
  data = webidl.converters.DOMString(data, prefix, "Argument 1");
  try {
    return op_base64_btoa(data);

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Normalize the input before atob: trim whitespace, strip a data: URI prefix up to the first comma, and translate base64url - to + and _ to /.
  2. Reject early when (length without trailing '=' padding) % 4 === 1 — it can never decode.
  3. Re-add stripped '=' padding to a multiple of 4 before calling atob.
  4. Wrap atob in try/catch on InvalidCharacterError so callers learn which payload was malformed.

Example fix

// before
const payload = atob(jwt.split('.')[1]); // base64url '-'/'_' -> InvalidCharacterError

// after
let s = jwt.split('.')[1].replaceAll('-', '+').replaceAll('_', '/');
while (s.length % 4 !== 0) s += '=';
const payload = atob(s);
Defensive patterns

Strategy: validation

Validate before calling

function toStdBase64(s) {
  let t = String(s).trim().replace(/^data:[^,]*,/, '')
    .replaceAll('-', '+').replaceAll('_', '/');
  if (t.replace(/=+$/, '').length % 4 === 1) throw new TypeError('invalid base64 length');
  while (t.length % 4 !== 0) t += '=';
  return t;
}
const bytes = atob(toStdBase64(input));

Type guard

const isStdBase64 = (s) =>
  /^[A-Za-z0-9+/]+={0,2}$/.test(s) && s.length % 4 !== 1;

Try / catch

try {
  decoded = atob(input);
} catch (e) {
  if (e instanceof DOMException && e.name === 'InvalidCharacterError') {
    return rejectToken(input);
  }
  throw e;
}

Prevention

When it happens

Trigger: atob('hello!') ('!' is not base64); atob('a') (length % 4 === 1 is impossible); atob(jwt.split('.')[1]) where the JWT segment is base64url containing '-' or '_'; atob of a value copied with a trailing newline or a 'data:image/png;base64,' prefix.

Common situations: Decoding JWT header/payload segments without base64url translation; decoding data URIs pasted with the MIME prefix; tokens/API keys copied from terminals including whitespace; interop with encoders that emit base64url or omit padding.

Understand the failure class


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/fca12ce8a729b6f4. Report an issue: GitHub.