Invalid Base64 String: Why Decoding Fails and How to Fix It

A Base64 decoder rejects a string for one of three reasons: a character outside the alphabet, a length that cannot be right, or padding in the wrong place.

Base64 uses 64 characters, A to Z, a to z, 0 to 9, + and /, plus = as padding at the end (RFC 4648, section 4). Anything else makes a strict decoder fail. The confusing part is that decoders disagree about what "anything else" includes: a Base64URL string such as PDw_Pz8-Pg== decodes correctly with Node.js Buffer, fails in .NET and silently turns into the wrong bytes in Python. Knowing what your decoder tolerates is most of the fix.

The error in each language

WhereWhat you get
Chrome and Edge, atob()InvalidCharacterError: Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.
Node.js, atob()InvalidCharacterError: Invalid character, or The string to be decoded is not correctly encoded. for a wrong length
Python, base64.b64decode()binascii.Error: Incorrect padding, Invalid base64-encoded string: number of data characters (5) cannot be 1 more than a multiple of 4, or with validate=True, Only base64 data is allowed
.NET, Convert.FromBase64String()FormatException: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.
Java, Base64.getDecoder().decode()IllegalArgumentException: Illegal base64 character followed by the character code in hex, such as 2d for -
PHP, base64_decode($s, true)No message: the function returns false
AWS CLI version 2Invalid base64: followed by the value you passed

Node.js Buffer.from(s, 'base64') is the odd one out: it never throws. It skips characters it does not recognise, so a damaged string produces damaged bytes without any error.

What each decoder tolerates

InputatobPython b64decode.NETPHP strict
Missing padding: SGVsbG8DecodesIncorrect paddingFailsDecodes
Extra padding: SGVsbG8==FailsDecodesFailsReturns false
Spaces or line breaksIgnoredIgnored (fails with validate=True)IgnoredIgnored
Base64URL - and _FailsDropped silently: wrong bytes or an errorFailsReturns false
Other characters: quotes, \, !, :FailsDropped silentlyFailsReturns false

Python's default mode deserves a warning. It discards every character outside the alphabet before decoding, so the letters of a prefix such as data:image/jpeg;base64, are decoded as if they were data. Depending on the length of the prefix you get a confusing error about the number of characters, or no error and several bytes of garbage in front of the real content. Pass validate=True when you want bad input to fail clearly.

Characters that do not belong

When a strict decoder reports an invalid character, the string almost always picked up something on its way to you:

  • Quotes or surrounding text. Copying "SGVsbG8=" with its quotes, or a whole "key": "value" line, adds characters that are not Base64.
  • A data URI prefix. In data:image/png;base64,iVBOR... only the part after the comma is Base64. Remove everything up to and including the comma.
  • JSON escapes. PHP's json_encode writes / as \/, and wrapped content such as a PEM certificate is stored with \n between lines. If you copy the raw JSON text instead of the parsed value, the backslashes come along. Parse the JSON first and decode the resulting string.
  • Base64URL characters. - and _ mean the string uses the URL-safe alphabet, as JWTs do. See the section below.
  • Invisible characters. A non-breaking space or zero-width space pasted from a chat app or a document is not ASCII whitespace, so even atob, which ignores ordinary spaces, rejects it.

One case produces no error at all. If a standard Base64 value is put into a query string without percent-encoding, each + turns into a space when the query is parsed. atob and Python then skip those spaces: ++++YWJj arrives as YWJj and decodes to abc, with the first three bytes silently gone. Encode the value with encodeURIComponent, or use Base64URL in URLs.

Wrong length and padding

Base64 turns every 3 bytes into 4 characters, so a padded string always has a length divisible by 4. Without padding, the last group has 2 or 3 characters. A remainder of 1 is impossible for any input: if length % 4 === 1, a character was lost or an extra one was added, and no amount of padding will fix it.

Padding may only appear at the end, and there are never more than two = signs. RFC 4648 requires padding unless the format that uses Base64 says otherwise, and several formats do: JWTs and PKCE code challenges omit it. To restore missing padding before handing the string to a strict decoder:

// JavaScript
const padded = s + '='.repeat((4 - s.length % 4) % 4);

# Python
padded = s + '=' * (-len(s) % 4)

Base64URL given to a standard decoder

Base64URL (RFC 4648, section 5) replaces + with - and / with _, and usually drops the padding. A standard decoder rejects both characters, but only strings that happen to contain one of them fail, so the bug looks random. A random 16-byte value becomes 22 Base64URL characters, 21 of which can be - or _. Each avoids both with probability 62/64, so (62/64) to the power of 21, about 51 percent, of such values decode fine and the other 49 percent fail. Convert the alphabet and restore the padding, or use a decoder built for the variant:

// Browser
const b64 = s.replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));

// Node.js
Buffer.from(s, 'base64url');

# Python
base64.urlsafe_b64decode(s + '=' * (-len(s) % 4))

// Java
Base64.getUrlDecoder().decode(s);

Python's urlsafe_b64decode still needs the padding, which is why the example adds it. The differences between the two alphabets are covered in Base64 vs Base64URL.

Line breaks from email and PEM files

MIME email bodies wrap Base64 at 76 characters per line, and PEM files such as certificates wrap at 64. atob, .NET and PHP skip the line breaks. Java's basic decoder does not: use Base64.getMimeDecoder() for wrapped input. For PEM, also remove the -----BEGIN ...----- and -----END ...----- lines, which are not Base64.

It decoded, but the result is wrong

  • The output is more Base64. The data was encoded twice. YUdWc2JHOD0= decodes to aGVsbG8=, which decodes to hello. Decode again.
  • Accented characters look like é. atob returns one character per byte, so UTF-8 text needs a second step: new TextDecoder().decode(Uint8Array.from(atob(s), c => c.charCodeAt(0))).
  • Python raises UnicodeDecodeError after decoding. The bytes are valid Base64 but not UTF-8 text, for example 'utf-8' codec can't decode byte 0xe9 for text saved as Latin-1. Decode with the right encoding, such as .decode('cp1252'), or treat the result as binary.
  • The output is unreadable symbols. The data is binary, such as an image, a PDF or compressed data. Save the bytes to a file instead of printing them.

AWS CLI: "Invalid base64"

AWS CLI version 2 expects binary parameters to be Base64 by default. Passing plain JSON to a command such as aws lambda invoke --payload '{"id": 1}' fails with Invalid base64. Add --cli-binary-format raw-in-base64-out to pass the text as it is, or encode the payload yourself.

Checking a string quickly

Paste the value into Base64 Decode. It removes whitespace and a data URI prefix, converts Base64URL characters and adds missing padding before decoding, so if it still says the input is not valid Base64, the string really contains a foreign character or has an impossible length. When it does decode, it shows UTF-8 text directly and offers anything else, such as an image, as a file to download.

Frequently Asked Questions

What does "Incorrect padding" mean in Python?

The string length is not a multiple of 4 and the = padding is missing, which is normal for Base64URL and JWT parts. Add = until the length is a multiple of 4. If the length leaves a remainder of 1, the string is damaged instead.

Why does atob throw InvalidCharacterError?

The string contains a character outside the Base64 alphabet, such as - or _ from Base64URL, a quote or a backslash, or it has extra padding or an impossible length. Spaces and line breaks are ignored and are not the cause.

Why does the same string decode in one language and fail in another?

Decoders differ in strictness. Python and Node.js Buffer skip unknown characters by default, PHP strict mode and atob accept missing padding, and .NET rejects both missing padding and unknown characters.

How do I decode Base64URL?

Replace - with + and _ with /, add = padding to a multiple of 4, then decode normally. Node.js accepts it directly with Buffer.from(s, 'base64url').