SharePoint REST uploads: why Base64 bodies become corrupted images
You upload an image to a SharePoint document library through the classic REST endpoint — POST /_api/web/GetFolderByServerRelativeUrl('...')/Files/add(url='logo.png',overwrite=true) — and SharePoint answers 201 Created. Everything looks fine until someone opens the file: the image is broken, or it opens in a text editor as a wall of Base64 characters.
This is the signature of a body-encoding mismatch: you sent Base64 text, and SharePoint wrote exactly that text into the file. The Files/add endpoint does not decode Base64 — whatever bytes arrive in the request body become the file, byte for byte.
The symptom: 201 Created, broken file
The failure mode is uniquely misleading. The upload succeeds:
- the endpoint answers
201 Createdwith the new file's metadata; - the file appears in the library with the right name and roughly the right size (~33% larger than the original image — the Base64 overhead);
- previews may or may not generate, and the file will not open as an image anywhere.
Nothing errors, so nothing prompts you to look at the body. Compare with Trello's 400 on the same mistake — SharePoint is quieter, which is why this page exists.
The root cause: the body is written as-is, byte for byte
Files/add treats the request body as the raw content of the file. The documented contract sends the file's binary bytes directly as the body, with Content-Type: application/octet-stream-style handling and a valid form digest (X-RequestDigest) when called from browser-authenticated contexts.
Base64 is not a file format; it is a text encoding of bytes. When the body contains that text:
- SharePoint does not look for a
Content-Transfer-Encodingheader and does not attempt any decode; - each ASCII character of your Base64 string becomes one byte of the stored file;
- the stored bytes are your encoded text, not the original image.
The result is a file that is ~1.33× the size of the original, whose first bytes are the letters iVBOR (a PNG's Base64 prefix) instead of the PNG magic bytes. One hex dump of the uploaded file tells the whole story — if you see Base64 alphabet characters where you expect binary, this is the bug. The same "stored the text, not the bytes" behavior exists across cloud-storage APIs; the OneDrive failure mode is nearly identical.
Copy-paste fixes
The fix is always the same shape: decode once on your side, then send the raw bytes as the body.
curl: --data-binary
# 1. Get a digest token (needed for cookie/ADFS-style auth; app-only bearer tokens can skip this)
DIGEST=$(curl -s -X POST \
"https://contoso.sharepoint.com/sites/mysite/_api/contextinfo" \
-H "Authorization: Bearer ${SP_ACCESS_TOKEN}" \
-H "Accept: application/json;odata=verbose" \
| python3 -c "import sys,json;print(json.load(sys.stdin)['d']['GetContextWebInformation']['FormDigestValue'])")
# 2. Upload the raw bytes — --data-binary, never -d on text
curl -s -X POST \
"https://contoso.sharepoint.com/sites/mysite/_api/web/GetFolderByServerRelativeUrl('Shared%20Documents')/Files/add(url='logo.png',overwrite=true)" \
-H "Authorization: Bearer ${SP_ACCESS_TOKEN}" \
-H "Accept: application/json;odata=verbose" \
-H "Content-Type: application/octet-stream" \
-H "X-RequestDigest: ${DIGEST}" \
--data-binary @logo.png
The two details that matter: --data-binary (which sends the file bytes untouched — plain -d strips newlines) and no transformation of the body anywhere in the pipeline.
Node.js 18+
// node sp-upload.mjs
import { readFile } from "node:fs/promises";
const site = "https://contoso.sharepoint.com/sites/mysite";
const folder = "Shared Documents";
const token = process.env.SP_ACCESS_TOKEN;
if (!token) throw new Error("Set SP_ACCESS_TOKEN");
const bytes = await readFile("logo.png");
const url = `${site}/_api/web/GetFolderByServerRelativeUrl('${folder}')/Files/add(url='logo.png',overwrite=true)`;
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json;odata=verbose",
"Content-Type": "application/octet-stream",
},
body: bytes, // raw bytes — not a string, not JSON
});
if (!res.ok) throw new Error(`SharePoint ${res.status}: ${await res.text()}`);
const data = await res.json();
console.log(data.d.ServerRelativeUrl);
Passing bytes (a Buffer) keeps the body binary. If instead you do body: bytes.toString("base64") — or JSON.stringify anything — you have just created the corrupted-file bug this page is about.
Python: requests
import requests
site = "https://contoso.sharepoint.com/sites/mysite"
folder = "Shared Documents"
token = "YOUR_ACCESS_TOKEN"
with open("logo.png", "rb") as f:
r = requests.post(
f"{site}/_api/web/GetFolderByServerRelativeUrl('{folder}')/Files/add(url='logo.png',overwrite=true)",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/json;odata=verbose",
"Content-Type": "application/octet-stream",
},
data=f, # file object: requests streams the raw bytes
timeout=60,
)
r.raise_for_status()
print(r.json()["d"]["ServerRelativeUrl"])
data=f (the file object) sends raw bytes. data=f.read() works too; json= or a Base64 string does not.
Browser: send ArrayBuffer, not a string
In browser code, the corruption usually arrives through jQuery's default request transforms or through building a binary string with String.fromCharCode. Send the ArrayBuffer directly and disable transforms:
const reader = new FileReader();
reader.onloadend = async () => {
await fetch(endpoint, {
method: "POST",
headers: {
Accept: "application/json;odata=verbose",
"X-RequestDigest": document.getElementById("__REQUESTDIGEST").value,
},
body: reader.result, // ArrayBuffer — body stays binary
});
};
reader.readAsArrayBuffer(fileInput.files[0]);
Historical note for older codebases: jQuery's default transformRequest serializes the body, which corrupts binary uploads; the classic fix is transformRequest: [] (an empty transform chain) together with processData: false. Modern fetch with an ArrayBuffer/Blob body avoids the whole class of bugs.
Start from a Base64 string
If your image exists as Base64 — because you encoded it with the Image to Base64 tool or received it from an upstream service — decode it before the request:
const bytes = Buffer.from(rawBase64, "base64"); // decode exactly once
// ...then send `bytes` as the request body
Strip a data:image/png;base64, prefix first if present: everything before the first comma is metadata, and leaving it in makes the decode fail or produce junk. If the decoded file still will not open, the string itself may be damaged — whitespace, truncation, or double encoding; fix invalid Base64 image data is the checklist for that.
Note the asymmetry with reading: SharePoint can hand you file content in different shapes (the /$value endpoint returns raw binary), but writing has exactly one honest format — the bytes.
Pitfalls and a pre-flight checklist
201 Createdproves nothing about content. Always verify by downloading the file and comparing sizes or hashing against the original.- No Base64 in the body. Ever. The endpoint does not negotiate encodings;
Content-Transfer-Encodingheaders are ignored, not honored. Content-Typedescribes the file, it does not transform the body. Settingimage/pngdoes not decode anything.- Beware string-based HTTP stacks. Anything that converts the body to a UTF-8 string before sending (older jQuery defaults, some ORM/HTTP wrappers) will corrupt binary uploads.
- URL-encode folder names in the endpoint (
Shared%20Documents), and keep the folder path inGetFolderByServerRelativeUrl('...')matching your site's real server-relative path. - Large files: this simple endpoint is fine for everyday assets; REST supports binary bodies up to 2 GB, and bigger jobs belong in the documented large-file patterns.
- Verify with a hash:
certutil -hashfile logo.png MD5(Windows) ormd5 logo.png(macOS/Linux) against the downloaded copy.
FAQ
Why does SharePoint return 201 if the content is garbage?
Because the request was valid — the body was stored, as-is. The endpoint has no schema for file contents; it cannot know that iVBOR... text was supposed to be a PNG. This is why 'it returned 201' is not a test.
Does adding Content-Transfer-Encoding: base64 help?
No. Unlike MIME email, REST file endpoints here ignore that header. The bytes you send are the bytes stored.
Can I upload Base64 through Microsoft Graph instead?
No — Graph has the same behavior on its OneDrive endpoints: it stores exactly what you PUT. The fix pattern is identical: decode, then send bytes.
Related guides
The OneDrive companion problem — Graph stores Base64 as-is — uses the same fix. For broken input strings, start with invalid Base64 image data; for JSON APIs that do accept Base64 text, see the API payload guide.