Base64 Line Breaks and Padding: Why They Break Decoders
2026-09-17 · 1036 words
Two formatting rules cause more Base64 failures than every other cause combined. Both are part of the standard, both are routinely violated in the wild, and strict decoders reject the results — which means the same string can work in one language and fail in another.
The first rule is that Base64 output is often wrapped across lines. The second is that the tail is padded with = to round the length up to a multiple of four. Neither changes the data. Both change the string, and decoders disagree about how forgiving to be.
Why 76 characters
Base64 predates JSON, HTTP/2 and email attachments. It was designed to survive transport through systems that cared about line length, and the dominant one was email: RFC 2045 wraps encoded bodies at 76 characters, a limit chosen to stay under SMTP's 998-octet line maximum while keeping quoted-printable output readable. Every four Base64 characters represent exactly three bytes, so 76 characters is 57 bytes per line — an awkward-looking number that is really just "72 characters plus a little headroom".
That is why you see this shape in PEM files, MIME bodies and log output:
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAF
AAH/q842iQAAAABJRU5ErkJggg==
The newline is not data. It is transport furniture that leaked into the copy-paste layer, and a decoder that treats the string as opaque bytes will stop at the first \n and report an illegal character at position 76.
Why the padding exists, and why the remainder matters
Each group of four Base64 characters encodes three bytes. When the input length is not a multiple of three, the encoder pads the output with =:
- 1 leftover byte → 2 characters +
== - 2 leftover bytes → 3 characters +
= - 0 leftover bytes → no padding
So the encoded length is always a multiple of four, once padding is included. Turn that around and you get the single most useful diagnostic rule in this whole area:
After normalisation, a complete Base64 string's length modulo 4 is 0, 2 or 3. A remainder of 1 is impossible.
A remainder of 1 does not mean "slightly malformed". It means characters are missing. There is no 1-byte remainder case in the encoding, so some input byte pair has been cut in half. In practice the cause is almost always truncation: a log line capped at 1024 characters, a terminal that wrapped and lost a line during selection, a database column with a VARCHAR(n) limit, or a copy that stopped a character early.
This is worth stating plainly because it is the difference between a fixable problem and an unfixable one. A decoder can repair missing padding — adding = is deterministic and lossless. It cannot repair a missing character; anything it produces will be a corrupted image, and a tool that "helpfully" pads and decodes a remainder-of-1 string is handing you a picture that is wrong in a way you cannot see.
What different decoders do about it
The behaviour you get depends entirely on which implementation you use, and the differences are not documented prominently anywhere.
Python is strict and explicit. base64.b64decode(s) ignores newlines but rejects other junk; adding validate=True makes it reject any character outside the alphabet:
import base64
raw = base64.b64decode(payload, validate=True) # binascii.Error on bad input
Node is the opposite extreme. Buffer.from(s, 'base64') silently discards characters it does not recognise. It will decode a string containing spaces, quotes and stray prose without complaint:
Buffer.from('iVBO\nRw0K Ggo=', 'base64').length // 10 — no error, junk skipped
That leniency is convenient when you know the input is nearly right, and dangerous when you do not: a truncated string still returns bytes, just fewer of them, and nothing tells you the difference.
Browsers sit in the middle. atob throws InvalidCharacterError on a newline — but only in strict spec-compliant form; historically it ignored whitespace. Normalising first removes the ambiguity:
const clean = payload.replace(/[\r\n\t ]+/g, '');
const bytes = Uint8Array.from(atob(clean), (c) => c.charCodeAt(0));
A normalisation order that does not lose information
The order matters, because some repairs change what the next check sees. This sequence is what our Base64 to Image tool applies, in this order, reporting each step it took:
- Strip the
data:prefix if present — everything up to the first comma. If there is no comma, there is no prefix. - Decode percent-encoding (
%3D→=). This happens before the alphabet check, otherwise%looks like an illegal character. - Remove whitespace and line breaks.
- Strip surrounding quotes, which appear when a string is copied out of JavaScript, JSON or a shell script.
- Unescape literal
\nand\tsequences, which appear when the encoded string was itself stored inside a JSON string. - Convert the URL-safe alphabet (
-→+,_→/) if those characters are present. - Fix padding — add the
=characters the length says are missing, or remove surplus ones. - Check the remainder. If it is 1, stop and tell the user the data is truncated. Do not decode.
Steps 7 and 8 are the pair that separates a useful decoder from a plausible one. Without step 8 you ship a corrupted image silently; without step 7 you reject every string that lost its padding in transit, which is a large fraction of them.
Deciding when to accept a repaired string
Repair is not the same as validation. Adding padding is safe because it is reversible and determined by the data itself. Stripping whitespace is safe for the same reason. But once you have repaired the form, the only real check on the content is structural: does the decoded byte stream look like the thing it is supposed to be?
That is why the tool reports what it changed rather than quietly fixing things. "Removed whitespace and line breaks (57 positions)" is information: it tells you the string went through a MIME-aware channel. "Added 2 padding characters" tells you the source dropped them — often a URL, a query parameter, or a JavaScript string that was trimmed. Both are normal, and both are worth knowing about when the image that comes out is not the image you expected.
The one repair that should never be silent is removing characters from the middle of the data. If a decoder has to throw away something other than whitespace to make a string valid, the honest answer is that the input is not Base64 — see what a data URI actually allows for the header half of this problem, and Base64 image not showing for the full troubleshooting order when you have already decoded something and it still will not display.