Skip to content

Media upload: files in a batch are uploaded concurrently and race on the branch head ("is at <sha> but expected <sha>") #432

Description

@willdarbey123-netizen

Summary

When several files are added to a media library in one go (multi-select in a file dialog, or dragging a selection onto the drop zone), every file is uploaded concurrently rather than one after another. Each upload is its own commit on the branch, so the concurrent requests race on the branch head and GitHub rejects all but one per round with:

... is at <sha> but expected <sha> - https://docs.github.com/rest/repos/contents#create-or-update-file-contents

The three SHAs in the errors are commit SHAs (the branch head moving), not blob SHAs. Some files in a batch land, the rest fail, and it gets worse the more files are selected. Uploading one file at a time works.

Cause

components/media/media-upload.tsx, in handleFiles:

for (const file of files) {
  const uploadPromise = (async () => { /* fetch POST /files/... */ })();

  await toast.promise(uploadPromise, { ... });
}

The loop is written to be sequential, but toast.promise() from sonner (^2.0.7 in package.json) does not return the promise. It returns the toast id synchronously, with an unwrap helper attached:

// sonner src/state.ts
return Object.assign(id, { unwrap });   // or { unwrap } when id is undefined

So await toast.promise(...) resolves immediately and the loop starts the next upload straight away. The uploadPromise IIFE has already begun its fetch by then, so all files in the batch are in flight together.

The rich-text image insert path (components/ui/editor/index.tsx, insertLocalImageFile) awaits onUploadImage directly, so it is not affected. components/folder-create.tsx:89 has the same await toast.promise(...) pattern but is a single call, not a loop, so it is harmless.

Fix

Await the upload itself, and keep going with the remaining files if one fails (the toast already reports the failure):

toast.promise(uploadPromise, { ... });

try {
  await uploadPromise;
} catch {
  // Already surfaced by the toast; continue with the next file.
}

PR to follow against development.

Environment

  • app.pagescms.org, observed 2026-09-08
  • GitHub backend, repository with a single main branch
  • Reproduces with as few as 2 files selected together; seen most often with 5 to 30

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions