Disclosure: I built the free File to Base64 / Base64 to File converter on base64decodetool.com that I use for checks in this post.
Sooner or later, every backend developer has to put a binary file inside JSON. An invoice PDF goes to an e-signature API, a scanned ID goes to a KYC provider, a generated report goes into a webhook. JSON has no binary type, so the file travels as a Base64 string.
It mostly works. When it breaks, though, you get a PDF reader saying "file is damaged" and nothing pointing at the cause. Here's what I check now: how much bigger the payload gets, whether to include the data: prefix, how to decode it safely, and the four corruption bugs I see most.
How much bigger does the file get?
Base64 (RFC 4648, section 4) turns every 3 bytes into 4 ASCII characters and pads the last group with =. So the encoded length is:
encoded_chars = 4 × ceil(original_bytes / 3)
That works out to about 33% overhead. Line breaks, JSON quotes and field names come on top of that.
| Original file | Base64 length | Note |
|---|---|---|
| 587 bytes (my demo PDF below) | 784 chars | +33.6% |
| 2,000,000 bytes (illustrative 2 MB scan) | 2,666,668 chars | about 2.67 MB of JSON text before compression |
For APIs, the practical effect is that a provider's "10 MB request limit" really means roughly 7.5 MB of actual file. Check your limit against the encoded size, not the size on disk.
Worked example: a tiny invoice PDF, encoded and decoded
Input (illustrative): I generated a minimal one-page PDF called invoice-demo.pdf that only contains the text "Invoice #0001". It has no real data. It's 587 bytes.
Step 1: Encode. Every PDF starts with the bytes %PDF-. Base64 of the header %PDF-1.4\n is JVBERi0xLjQK, so any Base64 string starting with JVBERi0 is almost always a PDF. That's a useful thing to recognise when you're reading logs.
import base64, json
raw = open("invoice-demo.pdf", "rb").read() # read as BYTES, never text
body = json.dumps({
"filename": "invoice-demo.pdf",
"content_type": "application/pdf",
"data": base64.b64encode(raw).decode("ascii"),
})
Step 2: Check the size. 4 × ceil(587 / 3) = 4 × 196 = 784 characters, and the string ends in a single = because 587 = 3 × 195 + 2 leaves two bytes over.
Step 3: Decode on the receiving side and verify.
data = json.loads(body)["data"]
pdf = base64.b64decode(data, validate=True) # reject stray characters instead of skipping them
assert pdf[:5] == b"%PDF-"
assert len(pdf) == 587
In Node, the equivalent is Buffer.from(json.data, "base64").
Result I checked: Python's base64 module gave 784 characters, starting JVBERi0xLjQK and ending NAolJUVPRgo=. The decoded bytes had the same SHA-256 as the original. I also ran the converter's own JavaScript (the same encode/decode functions the page loads) on the same file in Node. It produced the identical 784-character string. Decoding it back gave 587 bytes, detected as application/pdf with a .pdf extension.
Should you include the data: prefix?
A data URI (RFC 2397) looks like data:application/pdf;base64,JVBERi0xLjQK…. Browsers need it when you put a file in src or href. Most JSON APIs don't want it. They expect raw Base64 in one field and the MIME type in another (content_type, mimeType, and so on). Sending the prefix to an API that expects raw Base64 is a classic "invalid base64" error.
My rule of thumb is to send raw Base64 plus an explicit MIME field unless the API docs show a data: example.
On my tool, File to Base64 outputs raw Base64 with no prefix. In the other direction, Base64 to File accepts both forms: it strips everything up to ;base64, before decoding.
4 ways the file gets corrupted (and how each one looks)
I reproduced each of these with the demo PDF, using both Python and the tool's decoding logic.
1. + turned into a space. If the Base64 passes through a URL query string or an application/x-www-form-urlencoded body without escaping, every + becomes a space. My demo string has two + characters. Python's b64decode(..., validate=True) rejects the result ("Only base64 data is allowed"), and my tool's decoder reports "Invalid Base64 data". A more lenient decoder might quietly drop the spaces and give you shifted, garbage bytes. Fix: send it in a JSON body, or use Base64URL (- and _) on purpose.
2. Copying the escaped JSON from a log. If an encoder wraps lines at 76 characters and you copy the string from a raw log, you get literal \n sequences (a backslash plus the letter n). A decoder that skips "invalid" characters removes the backslash but keeps the n, which is a valid Base64 letter, so the data shifts. Fix: always run the payload through a JSON parser first. Real newlines are fine. My tool and most decoders ignore them.
3. Silent truncation. Log lines, VARCHAR(255)-style columns and some proxies cut long strings. That's the worst case, because truncated Base64 often still decodes. When I cut my 784-character string to 500 characters, it decoded "successfully" to 375 bytes, and it still starts with %PDF-, so magic-byte detection still says PDF. Only the size (375 vs 587) or a hash shows the problem. Fix: send size or sha256 alongside data, and check them after decoding.
4. Double encoding. One service Base64-encodes the file, and the next one encodes the resulting string again. Decode once and you get a text file that starts with JVBERi0x… instead of a PDF. My tool reports a generic application/octet-stream / .bin in that case, which tells you to decode once more. In the File to Base64 tab it also warns you when an uploaded file already looks like Base64 and offers a Decode This File button.
One more, which I didn't need a demo for: reading the PDF as a text string (for example response.text() in JavaScript, or open(path) without "rb") before encoding. Bytes that aren't valid UTF-8 get replaced, so the damage happens before Base64 is even involved.
Quick debugging checklist
- Does the string start with
JVBERi0(PDF),iVBORw0(PNG), or/9j/(JPEG)? If it starts withdata:, strip the prefix. - Is the length a multiple of 4 (with padding)? If not, it may be Base64URL or truncated.
- Does decoded size × 4/3 roughly match the string length?
- Does the decoded file's hash match what the sender logged?
How to use the tool
- Open base64decodetool.com/file/ and pick the File to Base64 tab. Click Browse Files (or drop a file, up to 50 MB), then Encode File to Base64, then Copy or Download (.txt).
- To go back, switch to Base64 to File. Paste into Paste Text (or use Upload File for a .txt), then click Decode Base64 to File.
- Check the detected MIME type and size, optionally Set Custom File Name, then click Download File.
The conversion runs in your browser. Even so, for anything sensitive I'd test with a dummy file like the one above rather than a real customer document.
Top comments (0)