Disclosure: I built the free Base64 Decoder on base64decodetool.com that I use for the checks in this post.
A Base64 string decodes fine in one service. Then someone copies it out of a log, a raw JSON body or a ticket, and it suddenly throws Invalid character or Incorrect padding. Sometimes it's worse: it decodes "successfully" into garbage. The string usually wasn't corrupted in transit. The copy-paste added or removed characters.
This post covers the four cases I see most and gives you one rule to check before you start guessing.
The one rule: length mod 4
Every 4 Base64 characters carry 3 bytes (4 × 6 bits = 24 bits). After you remove whitespace and trailing =, a valid string has this many characters:
-
4n: whole groups -
4n + 2: ends with 1 byte (normally padded with==) -
4n + 3: ends with 2 bytes (normally padded with=)
4n + 1 is never valid. One leftover character holds only 6 bits, which isn't enough for a byte. If you count 4n + 1, something was added or cut off.
Worked example: a JSON-escaped line break (illustrative data)
The payload is a made-up 74-byte JSON document:
{"order":"DEMO-1042","status":"shipped","note":"illustrative sample only"} Base64-encoded, it becomes 100 characters ending in one = (74 bytes → 25 groups → 100 chars, with 1 padding char because 74 = 3 × 24 + 2):
eyJvcmRlciI6IkRFTU8tMTA0MiIsInN0YXR1cyI6InNoaXBwZWQiLCJub3RlIjoiaWxsdXN0cmF0aXZlIHNhbXBsZSBvbmx5In0= A sanity check on the first group: e y J v are indexes 30, 50, 9, 47 in the alphabet. In bits that's 011110 110010 001001 101111. Regrouped into bytes, you get 123, 34, 111, which is {"o. So far, so good.
Now the producer wraps the string at 76 characters (the MIME line length from RFC 2045) and stores it in JSON. In the raw JSON text, the line break shows up as the two characters backslash and n:
"eyJvcmRlciI6IkRFTU8tMTA0MiIsInN0YXR1cyI6InNoaXBwZWQiLCJub3RlIjoiaWxsdXN0cmF0\naXZlIHNhbXBsZSBvbmx5In0=" You copy everything between the quotes and paste it into a decoder. Here's what happens, step by step:
- The backslash isn't a Base64 character, so lenient decoders drop it.
- The
nis a valid Base64 character, so it stays. - You're left with 101 characters (
...cmF0naXZl...), and 101 = 4 × 25 + 1. That length is impossible.
What I got when I checked:
| Decoder | Result |
|---|---|
| My tool's decode logic (run in Node) | Error: InvalidCharacterError from atob |
Python base64.b64decode(s) (default, lenient) | No error: 75 bytes, ...illustrat followed by binary junk |
Python b64decode(s, validate=True) | Error: Only base64 data is allowed |
A real newline instead of \n | All of them: the correct 74 bytes |
The fix is to decode the JSON first (JSON.parse, json.loads, jq -r) and then decode the Base64. A real newline is harmless. The literal two characters \n are not.
The silent version: escaped \r\n on unpadded Base64
This variant is nastier. Take a 69-byte illustrative sentence. Its Base64 has no padding (92 chars, a multiple of 4). Wrap it with an escaped Windows line ending, \r\n, and two stray letters, r and n, survive. 92 + 2 = 94 = 4n + 2, which is a valid length. The decoder pads it and returns 70 bytes. The text is correct up to illust, then turns into junk. My decoder doesn't throw here. It shows its amber notice instead: "Output contains replacement characters. Please select Hex or Binary if processing binary data." In a script, you'd get no warning at all. That's why you should compare the decoded size (or a hash) with what the sender reports.
Three more copy-paste traps
1. The data: prefix. data:application/json;base64,eyJv... isn't Base64. In Python, the letters of the prefix count as data, so I got number of data characters (125) cannot be 1 more than a multiple of 4. Strip everything up to ;base64, first. My decoder does this automatically.
2. Glued chunks. Two separately encoded pieces joined together, such as SGk= + SGk= → SGk=SGk=, put an = in the middle. Both atob and Python reject it. Decode each chunk separately and join the bytes, not the strings.
3. Mixed alphabets. A Base64URL string (- and _) pasted into a strict standard decoder fails. My decoder maps - → + and _ → / on every input, whether or not URL-Safe Format is switched on. That's convenient, but it means it won't warn you that someone mixed the two alphabets.
A strict decode helper (Python)
The helper accepts what's legitimately harmless (real whitespace, a data URI prefix, Base64URL, missing padding) and rejects everything else:
import base64, re def strict_b64decode(s: str) -> bytes: if ";base64," in s: # drop a data: URI prefix s = s.split(";base64,", 1)[1] s = re.sub(r"\s+", "", s) # real whitespace/newlines only s = s.replace("-", "+").replace("_", "/") # accept Base64URL too if len(s.rstrip("=")) % 4 == 1: raise ValueError("length is 4n+1: truncated or has a stray character") s += "=" * (-len(s) % 4) # restore missing padding return base64.b64decode(s, validate=True) # reject anything else On the examples above, it returns the correct 74 bytes for the clean, real-newline and data-URI inputs. It raises an error for the escaped \n, the escaped \r\n and the glued chunks.
How to use the tool
- Open the Base64 Decoder and make sure Decode Base64 is selected.
- Paste the string into the Base64 Decode box. With Live Conversion on, the result appears in Output String right away.
- If you see a red error or the replacement-character notice, switch Character Set to Hexadecimal to inspect the raw bytes, then Copy or Download (.txt) the result.
A few honest notes about the tool. Like most lenient decoders, it silently drops characters outside the Base64 alphabet. That's why the backslash in the example vanishes while the n stays. In Chrome, the red error text comes straight from the browser's atob rather than a friendlier message. History keeps up to 20 recent conversions (inputs of 500 characters or fewer, plus their outputs) in your browser's localStorage. If you've pasted anything sensitive, use Clear History. Everything runs client-side, and nothing is uploaded.
Recap
Count the characters before you guess. 4n + 1 means something was added or removed. Unescape JSON before you decode, strip data: prefixes, decode glued chunks separately, and check the decoded size, because some corruption decodes without any error.