Dropbox: files over 150 MB fail with 409 — upload sessions needed (tested patch included)

Author: StefanBallasCreated Sep 7, 2026Updated Sep 18, 2026
Labelsbug

What happened?

Dropbox's /files/upload endpoint only accepts files up to 150 MB (https://developers.dropbox.com/dbx-performance-guide). _writeFileFromRoot in src/fsDropbox.ts uses that endpoint for every file — the TODO there notes it — so any larger file fails with Response failed with a 409 code, and once three such files fail in one run the sync stops with too many errors, stop the remaining tasks. On a vault containing court-book PDFs of 300–800 MB this meant the sync could never complete.

I've written and tested a fix: files over 100 MiB go through filesUploadSessionStartfilesUploadSessionAppendV2 (repeated) → filesUploadSessionFinish in 32 MiB chunks (a multiple of 4 MiB, as the API requires for every chunk but the last), with the same path, overwrite mode and client_modified as the single-request path, each call wrapped in the existing retryReq so 429s are handled as before. Files at or below the threshold are unchanged, and download is unchanged.

Tested on a ~200 GB vault synced to a Dropbox app folder: the previously failing files of 354 MB, 373 MB and 478 MB, and seven files between 515 MB and 769 MB, all uploaded byte-for-byte, and the following sync completed cleanly. Not addressed: the file is still read into memory whole before upload, as before, so very large files remain memory-bound on mobile.

The change is about 90 lines in one file. I'm not in a position to sign the CLA, so I'm offering it here rather than as a pull request — please feel free to adopt or rework it as you see fit.

Diff against current master (src/fsDropbox.ts)
diff
diff --git a/src/fsDropbox.ts b/src/fsDropbox.ts
index 4e9db76..a87f988 100644
--- a/src/fsDropbox.ts
+++ b/src/fsDropbox.ts
@@ -31,6 +31,12 @@ export const DEFAULT_DROPBOX_CONFIG: DropboxConfig = {
   credentialsShouldBeDeletedAtTime: 0,
 };
 
+// Dropbox's /files/upload is documented for files under 150 MB;
+// use an upload session above this size (100 MiB leaves a safe margin).
+const DROPBOX_UPLOAD_SESSION_THRESHOLD = 100 * 1024 * 1024;
+// Chunk size for upload sessions: must be a multiple of 4 MiB, at most 150 MB.
+const DROPBOX_UPLOAD_SESSION_CHUNK_SIZE = 32 * 1024 * 1024;
+
 const getDropboxPath = (fileOrFolderPath: string, remoteBaseDir: string) => {
   let key = fileOrFolderPath;
   if (fileOrFolderPath === "/" || fileOrFolderPath === "") {
@@ -636,20 +642,25 @@ export class FakeFsDropbox extends FakeFs {
       .replace(/\.\d{3}Z$/, "Z");
 
     // in dropbox, we don't need to create folders before uploading! cool!
-    // TODO: filesUploadSession for larger files (>=150 MB)
-
-    await retryReq(
-      () =>
-        this.dropbox.filesUpload({
-          path: key,
-          contents: content,
-          mode: {
-            ".tag": "overwrite",
-          },
-          client_modified: mtimeStr,
-        }),
-      origKey // hint
-    );
+    // Dropbox's simple /files/upload endpoint only supports files up to
+    // 150 MB. Larger files must go through an upload session, sent in
+    // chunks (each chunk except the last must be a multiple of 4 MiB).
+    if (content.byteLength > DROPBOX_UPLOAD_SESSION_THRESHOLD) {
+      await this._writeLargeFileViaSession(key, content, mtimeStr, origKey);
+    } else {
+      await retryReq(
+        () =>
+          this.dropbox.filesUpload({
+            path: key,
+            contents: content,
+            mode: {
+              ".tag": "overwrite",
+            },
+            client_modified: mtimeStr,
+          }),
+        origKey // hint
+      );
+    }
 
     // we want to mark that parent folders are created
     if (this.foldersCreatedBefore !== undefined) {
@@ -663,6 +674,70 @@ export class FakeFsDropbox extends FakeFs {
     return await this._statFromRoot(key);
   }
 
+  /**
+   * Upload a large file using Dropbox upload sessions:
+   * upload_session/start -> append_v2 (repeated) -> finish.
+   * The final "finish" call carries the same commit info (path, overwrite
+   * mode, client_modified) that the simple upload path uses.
+   */
+  async _writeLargeFileViaSession(
+    key: string,
+    content: ArrayBuffer,
+    mtimeStr: string,
+    origKey: string
+  ): Promise<void> {
+    const total = content.byteLength;
+    const chunkSize = DROPBOX_UPLOAD_SESSION_CHUNK_SIZE;
+    const hint = `${origKey} (upload session, ${total} bytes)`;
+
+    // first chunk
+    let offset = Math.min(chunkSize, total);
+    const startRsp = await retryReq(
+      () =>
+        this.dropbox.filesUploadSessionStart({
+          close: false,
+          contents: content.slice(0, offset),
+        }),
+      `${hint}: start`
+    );
+    const sessionId = startRsp?.result?.session_id;
+    if (sessionId === undefined || sessionId === "") {
+      throw new Error(`${hint}: Dropbox did not return an upload session id`);
+    }
+
+    // middle chunks: keep at least one chunk for the finish call
+    while (total - offset > chunkSize) {
+      const end = offset + chunkSize;
+      const thisOffset = offset;
+      await retryReq(
+        () =>
+          this.dropbox.filesUploadSessionAppendV2({
+            cursor: { session_id: sessionId, offset: thisOffset },
+            close: false,
+            contents: content.slice(thisOffset, end),
+          }),
+        `${hint}: append at ${thisOffset}`
+      );
+      offset = end;
+    }
+
+    // last chunk + commit
+    const finalOffset = offset;
+    await retryReq(
+      () =>
+        this.dropbox.filesUploadSessionFinish({
+          cursor: { session_id: sessionId, offset: finalOffset },
+          commit: {
+            path: key,
+            mode: { ".tag": "overwrite" },
+            client_modified: mtimeStr,
+          },
+          contents: content.slice(finalOffset, total),
+        }),
+      `${hint}: finish at ${finalOffset}`
+    );
+  }
+
   async readFile(key: string): Promise<ArrayBuffer> {
     await this._init();
     if (key.endsWith("/")) {

What OS are you using?

Windows

What remote cloud services are you using? (Please choose the specified one if it's in the list)

Dropbox

Version of the plugin

0.5.25 (fix tested on a local build of 0.5.25 and rebased onto current master)

Version of Obsidian

1.13.7 (Windows)

Using password or not

  • Yes.

Ensure no sensitive information

  • I ensure that no sensitive information is submitted in the issue.

Source: remotely-save/remotely-save