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:

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:

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

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.