payloadcms / payloadcms/payload

Bulk upload: an interrupted upload is dropped from the retry queue and counted as a success

Open Beginner friendly
#18,161 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Bug
Dominant language
TypeScript
Stars
44.8k
Forks
4.2k
Avg merge
2d 21h
Merged PRs (30d)
53

Description

Describe the Bug

In the bulk upload drawer, if the POST for one file rejects — a dropped connection, an aborted request, a DNS blip mid-upload — that file is silently dropped from the retry queue and counted as a success. The user sees "Successfully saved N files" and the file is gone, with no indication anything failed.

In packages/ui/src/elements/BulkUpload/FormsManager/index.tsx, saveAllDocs wraps each upload in a try/catch that swallows the rejection (v3.89.0, lines 520-522):

      } catch (_) {
        // swallow
      }

Every path inside the try assigns currentForms[i].errorCount before moving on. The catch does not, so a rejected fetch leaves the form's errorCount at the 0 it was initialised with in the reducer's ADD_FORMS.

The retry queue is then rebuilt from exactly that field (lines 529-538):

      const remainingForms = []

      currentForms.forEach(({ errorCount }, i) => {
        if (errorCount) {
          remainingForms.push(currentForms[i])
        }
      })

      const successCount = Math.max(0, currentForms.length - remainingForms.length)
      const errorCount = currentForms.length - successCount

So the interrupted form is not pushed into remainingForms, and because successCount is derived by subtraction rather than from the documents actually created, it is counted as saved. toast.success('Successfully saved N files') fires, setSuccessfullyUploaded(true) runs, and if it was the only failure errorCount is 0 and the drawer closes. newDocs — the array actually appended to on a 201 — correctly does not contain it, which is what makes the mismatch visible.

Link to the code that reproduces this issue

https://github.com/payloadcms/payload/blob/v3.89.0/packages/ui/src/elements/BulkUpload/FormsManager/index.tsx#L520-L538

Reproduction Steps
  1. Open any upload-enabled collection and start a bulk upload with two or more files.
  2. Cause the upload POST for one of them to reject rather than return an error status. Offline-mode mid-save works; so does stubbing it in the console before clicking save:
const realFetch = window.fetch
let dropped = false
window.fetch = (input, init) => {
  const url = typeof input === 'string' ? input : input.url
  if (!dropped && init?.method === 'POST' && url.includes('/api/media')) {
    dropped = true
    return Promise.reject(new TypeError('Failed to fetch'))
  }
  return realFetch(input, init)
}
  1. Click save.

Expected: the interrupted file stays in the drawer as a failed form so it can be retried, and is not counted in the success total.

Actual: it disappears from the drawer, Successfully saved N files counts it, and no error is shown for it.

Which area(s) are affected? (Select all that apply)

area: ui

Environment Info
Payload: 3.88.0 (verified unchanged in 3.89.0)
Node.js: 22
Next.js: 15.4.11
Suggested fix

Give the catch the same responsibility every other branch has — mark the form failed so it survives into remainingForms:

      } catch (_) {
        currentForms[i].errorCount = currentForms[i].errorCount || 1
      }

Deriving successCount from newDocs.length instead of by subtraction would also make the count robust to any future branch that forgets to set errorCount.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in packages/ui/src/elements/BulkUpload/FormsManager/index.tsx at saveAllDocs and inspect the catch and remainingForms logic around lines 520-538. Reproduce the rejected POST with the described offline-mode or fetch stub, then verify the interrupted file remains available for retry and is excluded from the success count.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
85/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.