Notes › Tech Notes › Handing a Base64 string to someone else: 4 edits tried, 3 of them throw an error

Handing a Base64 string to someone else: 4 edits tried, 3 of them throw an error

Handing this page's Base64 output to someone else is tolerant of newlines, tabs and even the missing padding: all three browser decode paths return the original bytes, and 22 samples give only five prefix lengths. Swap the plus and slash for URL-safe characters and atob, a data URI and Image all fail together.

The trouble started after the string left this page

It began with an unnecessary cleanup. A colleague handed me a base64 string whose body had been wrapped onto a new line every 76 characters, and I deleted those breaks by hand, assuming no decoder could swallow them. They told me later that the breaks never mattered. So I took what this page produces and damaged it on purpose. The first edits were swallowed without complaint. The second kind produced the first real failure: someone had followed the URL-safe habit and written the last two alphabet symbols as - and _ instead of + and /, and atob answered at once with InvalidCharacterError. The gap between the two ends of that handoff is what this note measures.

One readAsDataURL call, and the prefix has only five measured lengths

Line 80 of the template holds a single readAsDataURL call, and the page never encodes anything itself. Each file gets exactly one freshly built FileReader, and its body is character for character identical to what btoa produces for the same bytes, padding signs included. The prefix is not built from the file name either: it is always data:, then the type the File object reports, then ;base64,. Across 22 samples the prefix length, comma included, took only five values: 22, 23, 25, 29 and 37.

When type comes back empty the browser writes application/octet-stream and inspects nothing. The same 70 bytes of PNG data with an empty type drop the prefix to the 37 character tier. Declaring 39587 bytes of genuine JPEG data as image/webp makes the prefix say image/webp, and the preview still decodes to 1200x900. Whether an image appears is decided by a decoder that reads the bytes, not by that label.

Measured prefix lengths, five tiers 22 23 25 29 37 image/png image/jpeg x-icon application/gzip octet-stream

Figure 1: the five measured prefix lengths with the comma included; the tallest tier is the fallback when type is empty.

Body length is exactly 4*ceil(n/3)

Once the browser owns the encoding, the character count is pure arithmetic. Four input sizes were measured and each body length equals the formula 4*ceil(n/3), with no rounding and no off by one.

Input bytes against output characters
Input (B)body (characters)Prefix with comma
709622
97121295223
395875278423
4194304559240837

The Base64 size field counts the body only, so the string you carry is longer by the prefix: a body of 96 characters becomes a full string of 118, and 12952 characters become 12975. Without that gap the difference looks like corruption.

Copy and download hand over the same unwrapped string

The maxlength on the textarea is null, and no branch renders only the first N characters, so a file of 4194304 bytes stays fully present as 5592408 characters, and switching display modes leaves the value unchanged. Drag the file onto Binary to Base64 and both exits behave identically; neither post processes anything. The copy path reports the exact character count of the box: 12952 and 5592408 were both measured, and the contains a newline test came back false every time. The download goes through new Blob with a byte count equal to the character count, no byte order mark and no trailing line break. Across 22 samples the complete strings contained 0 line breaks, so nothing here folds the output at 76 columns.

Newlines, spaces and missing padding: all three paths swallow them

Decoding what this page hands out is far more forgiving than intuition suggests. Taking the 12952 characters produced for photo_400 as the reference, inserting a newline every 76 characters gives 13122, switching to CRLF gives 13292, and all three routes tested, atob, a fetch of a data: URI and a fresh Image, still returned the original 9712 bytes. Spaces, tabs and blanks at either end give the same result. Padding is not required either: removing the trailing = leaves 12950 characters and still yields 9712 bytes, and the 70 byte PNG drops from 96 characters to 94 with the same result. The alphabet has no whitespace, so skipping it cannot create ambiguity, and the padding signs carry no payload.

But hyphens and underscores are not tolerated at all

That leniency ends exactly at whitespace and padding. Replace + with - and / with _ and every route fails at once: atob raises InvalidCharacterError, fetch raises TypeError, and Image fires its error event. Of the 4 samples pushed through this, 3 failed outright, and the survivor was a 100 byte plain text sample whose body happened to contain neither + nor /, so the substitution left every character untouched. Success that proves nothing.

The subtler failure lives in the query string. The body of photo_400 carries + 119 times and / 139 times; push it through URLSearchParams and read it back, and the length is still 12952 while the content no longer equals what you started with, because every plus sign has been decoded into a space. Then atob throws, on a string that still looks exactly as long as the one you trusted. A length check is worthless here.

Same body, five ways of handing it over Untouched 12952 Back to 9712 bytes Newline per 76 chars 13122 Back to 9712 bytes Switched to CRLF 13292 Back to 9712 bytes Trailing = dropped 12950 Back to 9712 bytes Plus and slash rewritten -_ All three paths throw Whitespace tolerated, swapped alphabet not

Figure 2: the decode result after each edit; the four green rows are the variants that still come back intact.

What each on-screen statistic actually counts

The three numbers on the panel are computed independently and are not interchangeable. formatSize (lines 140-142) divides by 1024 and always prints two decimals for KB and MB, so 9712 bytes displays as 9.48 KB. The Base64 size field (lines 90-91) reports the character count of the body and excludes the prefix. The growth figure (line 92) is an increment ratio, measured from 0.0% up to 50.0%: a 16 byte input displays 50.0%, 70 bytes displays 37.1%, 4194304 bytes displays 33.3%, and an empty file is pinned to 0.0%. The displays cannot reconstruct each other: 12.65 divided by 9.48 gives 1.3344, while that same row shows growth as 33.4%.

This page has no decode entry point

Everything above describes how a browser treats a string this page produced; none of it is a feature the page offers. The only real input control is the file picker on line 11, an input type=file whose accept is null. The output box on line 35 is readonly, and no second control accepts typed text. Both the drag handler and the picker use only the first file in the list (lines 170 and 155), and there is no batch mode. Turning a base64 string back into a file needs a tool pointed in the other direction. Why the encoded form grows, and where the trailing = come from, is already kept in why Base64 encoding makes files bigger.

Other inputs that make the page look broken

A wrong extension or a corrupted picture never produces an error message. A 72 byte PNG fragment still yields 96 characters and still opens the preview container; handing the whole string to Image fires an error, but on screen you only get the browser's broken image placeholder, because line 64 checks nothing except whether type begins with image/ and never looks at the decode result. In reverse: PNG bytes declared as text/plain keep the container closed, yet the string still decodes to a 1x1 image when fed to Image directly.

The empty file is stranger. In plain Base64 mode the body has length 0, and the copy and download guards on lines 117 and 124 both short circuit, so the button appears dead. Switch to the Data URI form and there are suddenly 37 characters, the copy path accepts them and delivers 37, and the download is a 37 byte text file whose entire content is the prefix.

A few more things that keep coming up

Q: Do I have to strip the line breaks before pasting the string elsewhere?

Not on the browser side. Folding 12952 characters into 13122 or 13292 still returns the original 9712 bytes through every path tested. Transport is the layer that hurts: once the string enters a query string, each plus sign becomes a space before your code looks at it.

Q: Is it safe to rewrite the string into URL-safe form before sending it?

Only if you know which alphabet the receiver decodes against. In this run 3 of the 4 samples raised an error as soon as they were rewritten into -_, and a browser will not recognise the alternate symbols on your behalf. A receiver that expects URL-safe input has to substitute back itself, before any decoder sees the text.

Q: Why does the copy button do nothing for an empty file?

The guard on line 117 fires first whenever the value has length 0, so the clipboard is never called even once. The Data URI form offers 37 characters to copy, and those 37 are entirely prefix, which usually means the file was empty rather than that the page failed.

📎 Related articles

I Tested 16 Switch Combinations of an HTML Tag Stripper: It Reports 19 Tags but Deletes 148 Characters 2026-10-30 Linked Images Vanish When SVG Turns to PNG: A 594 Byte File Shows 3 Pictures in Preview and 1 in the Export 2026-10-30 App Store Screenshots Export at 1242×2688: 8 Canvas Presets, Noise at 24.83 Times the Size, and a Phone Frame Cropped 134.93 px by Default 2026-10-30 16 Keys In, 4 Columns Out: How json-to-excel Builds CSV Headers 2026-10-30 Untick Keep Comments and format-js Compresses a 24 Character URL Down to 13 Broken Characters 2026-10-30