OneDrive via Microsoft Graph: your Base64 upload is not decoded
You upload an image to OneDrive through Microsoft Graph — PUT /drives/{drive-id}/items/{item-id}:/logo.png:/content with your Base64 string in the body — and the API answers 201 Created. The file appears in OneDrive with the right name. But it is about 37% bigger than the original image, preview shows nothing, and nothing can open it.
You found the one cloud-storage API in this series that documents its own behavior in a Microsoft engineer's own words: Graph ignores transfer encodings and stores the request stream exactly as received. Your Base64 text did not become an image; it became a text file with an image's name.
The symptom: the file lands, ~37% larger, and will not open
The mismatch has two measurable fingerprints:
- Size. Base64 encodes 3 bytes into 4 characters, so the stored file is ~1.33x the original (4/3 = 1.333...). An originally 300 KB PNG lands as ~400 KB. That +37% is the tell.
- Content. Open the file in a text editor: you will see
iVBOR(or/9j/for JPEG) instead of binary garbage. Those are Base64 alphabet characters — the encoded text, stored verbatim.
On the multipart variant the same trap hides one level deeper: developers set Content-Transfer-Encoding: base64 on a part and assume the service will decode. A OneDrive team engineer confirmed it does not — the header is ignored (described as looking like a bug on their side), and the encoded data is stored raw, so the file uploads "successfully" but cannot be previewed.
The root cause: Graph stores the stream, it does not decode it
The simple-upload contract is minimal: the body of the PUT is the file content. The documented request body is "the binary stream of the file to be uploaded". There is no negotiation of encoding, no Content-Transfer-Encoding handling, no auto-detection of Base64. Whatever byte sequence arrives becomes the stored file, byte for byte.
That design is why the failure is silent. The request is well-formed; the server has no schema to know your text was supposed to decode into something else. This is the same class of behavior as SharePoint's Files/add endpoint — if you support both Microsoft surfaces, the fix is identical on each: decode once client-side, send raw bytes.
Two Graph-specific details worth knowing:
- Simple upload is capped at 250 MB. Graph's current documentation for
PUT /contentsupports files up to 250 MB; larger files go through an upload session (POST /createUploadSession) with PUTs of raw byte ranges — at most 60 MiB per request, sizes a multiple of 320 KiB. The session approach never accepts Base64 either. - The path syntax matters.
root:/folder/file.png:/contentaddresses the item by path;items/{item-id}:/file.png:/contentby ID. Both take the same raw-byte body.
Copy-paste fixes
The fix pattern: decode once on your side, then PUT the raw bytes.
curl: PUT with --upload-file
curl -s -X PUT \
"https://graph.microsoft.com/v1.0/drives/${DRIVE_ID}/root:/logos/logo.png:/content" \
-H "Authorization: Bearer ${GRAPH_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--upload-file logo.png
--upload-file (-T) sends the file bytes as the PUT body, untouched. Do not wrap the file in JSON, do not Base64 it again, do not add transfer-encoding headers.
Node.js 18+
// node onedrive-upload.mjs
import { readFile } from "node:fs/promises";
const token = process.env.GRAPH_TOKEN;
const driveId = process.env.DRIVE_ID;
if (!token || !driveId) throw new Error("Set GRAPH_TOKEN and DRIVE_ID");
const bytes = await readFile("logo.png");
const res = await fetch(
`https://graph.microsoft.com/v1.0/drives/${driveId}/root:/logos/logo.png:/content`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/octet-stream",
},
body: bytes, // raw bytes — the body IS the file
}
);
if (!res.ok) throw new Error(`Graph ${res.status}: ${await res.text()}`);
const item = await res.json();
console.log(item.id, item.size); // size should match logo.png exactly
The body must be the Buffer itself. body: bytes.toString("base64") re-creates the bug; body: JSON.stringify({ content: ... }) creates a JSON file whose name ends in .png.
Python: requests
import requests
token = "YOUR_GRAPH_TOKEN"
drive_id = "YOUR_DRIVE_ID"
with open("logo.png", "rb") as f:
r = requests.put(
f"https://graph.microsoft.com/v1.0/drives/{drive_id}/root:/logos/logo.png:/content",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/octet-stream",
},
data=f, # raw bytes, streamed
timeout=120,
)
r.raise_for_status()
print(r.json()["size"]) # equals the on-disk size in bytes
data=f streams the bytes; json= or a Base64 string produces exactly the corrupted-file behavior this page describes.
Start from a Base64 string
If the image reaches your code as Base64 — from the Image to Base64 tool, a webhook, or a message queue — decode it before the PUT:
import base64
image_bytes = base64.b64decode(raw_base64) # decode exactly once
# ...then PUT image_bytes as the request body
Node:
const bytes = Buffer.from(rawBase64, "base64");
// PUT `bytes` as body
Strip a data:image/png;base64, prefix first — everything before the first comma is metadata. If the decoded file still will not open or Graph rejects it, the string itself may be damaged (whitespace, wrong padding, double encoding); fixing invalid Base64 image data is the diagnostic checklist. And if what you actually need is to hand an image to a model, the payload shapes differ again — the API payload guide shows where Base64 text is the right format.
Pitfalls and a pre-flight checklist
201 Createdis not content validation. Checkitem.sizeagainst the on-disk file — if it is ~1.33x bigger, you uploaded the encoded text.- No
Content-Transfer-Encoding. Graph does not honor it on simple or multipart uploads; do not set it. - 250 MB ceiling on simple PUT. Bigger files:
createUploadSession, then PUT raw-byte ranges (at most 60 MiB per request, multiples of 320 KiB, 5-10 MiB recommended). Each chunk is raw bytes too. - Body must be bytes. Not a JSON envelope, not a string of Base64, not a form-data wrapper on the simple endpoint.
- Path conflicts: uploading to a path that already exists overwrites it (check
@microsoft.graph.conflictBehaviorsemantics if you need rename/fail behavior). - Verify by download: GET the file's
/contentand hash both sides (certutil -hashfile logo.png MD5/md5 logo.png) — matching hashes are the only real proof.
FAQ
Does Graph decode Base64 if I set Content-Transfer-Encoding: base64?
No. A OneDrive team engineer confirmed the header is ignored and the encoded data is stored as-is. Decode on your side.
Why is my uploaded file about 37% bigger than the original?
Because the stored content is the Base64 text, which encodes every 3 bytes as 4 characters. That size delta is the reliable fingerprint of an undecoded upload.
Is this the same for upload sessions?
Yes. Session chunks are raw byte ranges of the file; there is no Base64 mode anywhere in the upload path.
Related guides
The SharePoint REST equivalent — Files/add returning 201 with a broken image — shares the fix. For string diagnostics use invalid Base64 image data, and for APIs that do take Base64 in JSON see the API payload guide.