A common claim about Base64 in the browser is that the input length must be a multiple of 4. It isn't. atob decodes unpadded input fine. What it rejects is something else.
Here are all five cases, run:
"YQ==" len % 4 = 0 -> "a"
"YQ" len % 4 = 2 -> "a" <- no padding, still decodes
"YWJjZGU" len % 4 = 3 -> "abcde" <- decodes
"YWJjZ" len % 4 = 1 -> throws
"YQ=" len % 4 = 3 -> throws <- half-padded
So the rule has nothing to do with multiples of 4.
- Remainder 2 or 3 decodes without padding. The leftover bits are enough to reconstruct the bytes.
- Remainder 1 always fails. Six bits cannot encode a byte, so an encoder can never produce that length in the first place.
-
If you pad, pad fully.
"YQ="is rejected even though its length mod 4 is 3 — the same remainder as"YWJjZGU", which passes.
The actual contract of atob is "pad correctly, or do not pad at all", not "length must be a multiple of 4".
Where this bites
The rule is not the same across languages. Same input, "YQ":
JavaScript atob("YQ") -> "a"
Python base64.b64decode("YQ") -> binascii.Error: Incorrect padding
Go base64.StdEncoding.DecodeString("YQ") -> illegal base64 data at input byte 0
JavaScript is the lenient one and Go is the strictest. A value that round-trips fine in the browser and is then handed to a Python or Go service is a common way to get an error that looks like it came from nowhere — the data was never wrong, the two decoders just disagree about what counts as well-formed.
If you generate Base64 in the browser and consume it elsewhere, normalize the padding before it crosses the boundary, not after.
Verified
Node v23.7.0, Python 3, Go 1.22.0, run on 2026-09-14. The longer write-up — including Go's four encodings, and why picking the wrong one can pass every test and still break in production on one payload — is here: https://base64.withuse.io/base64-in-go/
Top comments (0)