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
| Where | What 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 2 | Invalid 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
| Input | atob | Python b64decode | .NET | PHP strict |
|---|---|---|---|---|
Missing padding: SGVsbG8 | Decodes | Incorrect padding | Fails | Decodes |
Extra padding: SGVsbG8== | Fails | Decodes | Fails | Returns false |
| Spaces or line breaks | Ignored | Ignored (fails with validate=True) | Ignored | Ignored |
Base64URL - and _ | Fails | Dropped silently: wrong bytes or an error | Fails | Returns false |
Other characters: quotes, \, !, : | Fails | Dropped silently | Fails | Returns 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_encodewrites/as\/, and wrapped content such as a PEM certificate is stored with\nbetween 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 toaGVsbG8=, which decodes tohello. Decode again. - Accented characters look like
é.atobreturns 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
UnicodeDecodeErrorafter decoding. The bytes are valid Base64 but not UTF-8 text, for example'utf-8' codec can't decode byte 0xe9for 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.