You copy a Base64 string out of a log file, an API response, or an email header, paste it into your decoder, and get one of these:
binascii.Error: Incorrect paddingThe input is not a valid Base-64 stringInvalidCharacterError: The string to be decoded is not correctly encoded- or worse, it "works" and gives you
hélloinstead ofhéllo
The string is rarely garbage; usually something changed it on the way. This post covers what makes a Base64 string valid, the five problems I run into most, and a small normalizer in Python and JavaScript that fixes the common cases.
What "valid Base64" actually means
Standard Base64 (RFC 4648 §4) turns every 3 bytes of input into 4 characters from a 64-character alphabet:
A-Z a-z 0-9 + / If the input length isn't a multiple of 3, the output is padded with = (one or two of them) so the total length is a multiple of 4. A valid standard string therefore has:
- Only alphabet characters, plus
=at the very end - At most two
=, and only as trailing padding - A length divisible by 4
Here's a quick check in Python:
import base64 print(base64.b64encode(b"Hello, World!")) # b'SGVsbG8sIFdvcmxkIQ==' 13 bytes of input is 4 full groups of 3 plus 1 leftover byte, so you get 20 characters, the last two of which are ==.
There's also a URL-safe variant (RFC 4648 §5), often called Base64URL, which swaps + for - and / for _ and usually drops the padding. The same bytes encode differently:
print(base64.b64encode(b"\xfb\xff\xbf")) # b'+/+/' print(base64.urlsafe_b64encode(b"\xfb\xff\xbf")) # b'-_-_' Keep that pair in mind. It comes up again below.
A worked example: watching strict decoding fail
Python's b64decode has a validate=True flag that makes it strict. Here's what it says about some typical broken inputs:
import base64, binascii for s in ["SGVsbG8", "SGVs bG8=", "-_-_", "SGV=sbG8", "SGVsbG8="]: try: print(repr(s), base64.b64decode(s, validate=True)) except binascii.Error as e: print(repr(s), "ERR", e) Output:
'SGVsbG8' ERR Incorrect padding 'SGVs bG8=' ERR Only base64 data is allowed '-_-_' ERR Only base64 data is allowed 'SGV=sbG8' ERR Excess data after padding 'SGVsbG8=' b'Hello' Each failure has a different root cause.
The five usual suspects
1. Missing padding. SGVsbG8 is Hello with its = stripped. JWTs, many APIs, and most Base64URL encoders drop padding on purpose. The fix is mechanical: append = until len % 4 == 0. One catch: a remainder of 1 can never be fixed by padding. A single leftover character carries only 6 bits, which isn't enough for a byte, so the string was truncated.
2. Whitespace and line breaks. MIME (email) wraps Base64 at 76 characters, PEM files wrap at 64, and copy-paste from terminals adds \r\n. Python's default (non-strict) decoder silently drops these characters, while strict decoders reject them. Strip all whitespace before validating.
3. URL-safe characters in a standard decoder. If you see - or _, it's Base64URL. Translate - → + and _ → /, then fix the padding.
4. A data URI prefix. Strings like data:text/plain;base64,SGVsbG8sIFdvcmxkIQ== include a header that isn't Base64. Drop everything up to and including the first comma.
5. URL encoding on top. If the string went through a query string, = may now be %3D and + may be %2B, or even a space. URL-decode first. A % sign is never valid Base64.
And one that isn't a validation error at all:
6. Wrong character set when turning bytes into text. The browser's atob() returns one character per byte (effectively Latin-1), so UTF-8 text with multi-byte characters gets mangled:
atob("aMOpbGxv"); // "héllo" — the bytes are right, the interpretation is wrong The Base64 was fine. The bytes are 68 c3 a9 6c 6c 6f, which is héllo in UTF-8. Decode the bytes with TextDecoder instead. Also remember that valid Base64 doesn't have to be text: it can just as easily be a PNG, a ZIP, or ciphertext.
A small normalizer
Python:
import base64, re STD = re.compile(r"[A-Za-z0-9+/]*={0,2}") def normalize_b64(s: str) -> bytes: s = s.strip() if s.startswith("data:"): s = s.split(",", 1)[1] # drop data URI header s = re.sub(r"\s+", "", s) # drop whitespace/newlines s = s.replace("-", "+").replace("_", "/") # URL-safe -> standard s = s.rstrip("=") if len(s) % 4 == 1: raise ValueError("impossible length: one leftover character") s += "=" * (-len(s) % 4) # restore padding if not STD.fullmatch(s): raise ValueError("illegal characters") return base64.b64decode(s, validate=True) print(normalize_b64("SGVsbG8")) # b'Hello' print(normalize_b64("SGVs\r\nbG8=")) # b'Hello' print(normalize_b64("-_-_")) # b'\xfb\xff\xbf' print(normalize_b64("aMOpbGxv").decode("utf-8")) # héllo JavaScript (browser or Node 16+):
function decodeBase64Utf8(input) { let s = input.trim(); if (s.startsWith("data:")) s = s.slice(s.indexOf(",") + 1); s = s.replace(/\s+/g, "").replace(/-/g, "+").replace(/_/g, "/").replace(/=+$/, ""); if (s.length % 4 === 1) throw new Error("Impossible length"); s += "=".repeat((4 - (s.length % 4)) % 4); if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s)) throw new Error("Illegal characters"); const bytes = Uint8Array.from(atob(s), (c) => c.charCodeAt(0)); return new TextDecoder("utf-8", { fatal: true }).decode(bytes); } decodeBase64Utf8("aMOpbGxv"); // "héllo" decodeBase64Utf8(decodeURIComponent("SGVsbG8%3D")); // "Hello" decodeBase64Utf8("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=="); // "Hello, World!" With fatal: true, the decoder throws when the bytes aren't valid UTF-8 (for example +/+/, which decodes to fb ff bf). That's a useful signal that you're looking at binary data, not a broken string.
When to be strict and when to be forgiving
- Be strict at trust boundaries. If you're accepting Base64 from users or other services, validate with strict rules and reject bad input with a clear error. Forgiving decoders can hide real corruption.
- Be forgiving when debugging. When you're staring at a string from a log, normalize first, then decode.
- Don't treat "decodes successfully" as "is Base64". The string
testis valid Base64 (it decodes to three bytes,b5 eb 2d). Lots of ordinary words pass the check. If you need to know whether a field is meant to be Base64, rely on your schema, not a guess.
Checking a string quickly
When I just want to know what's wrong with a string, I paste it into the Base64 Validator. It checks the character set, padding, length, and whether the string actually decodes, and it handles standard RFC 4648 §4, URL-safe §5, and data URI prefixes. The report shows the detected variant, padding status, character length, decoded byte size, and a text preview. Everything runs client-side in the browser, so nothing you paste is sent to a server.
Disclosure: I'm Nagesh and I built base64decodetool.com. If you hit a Base64 failure that isn't covered here, tell me in the comments. I'd like to add it to the checklist.
