payloadcms/payload · error · APIError

Staged upload is incomplete.

Error message

Staged upload is incomplete.

What it means

Thrown by `getStagedFile` when the temp file exists on disk but its byte length does not equal `upload.filesize`. This means the PUT that was supposed to deliver the bytes either did not complete or wrote a partial file.

Source

Thrown at packages/payload/src/uploads/stagedUpload.ts:183

  const upload = await verifyUploadID(req, uploadId)

  if (upload.collectionSlug !== collectionSlug || upload.user !== getUser(req)) {
    throw new Forbidden(req.t)
  }

  const directory = await getUploadDirectory(req, upload.collectionSlug)
  const tempFilePath = path.join(directory, upload.id)
  let data: Buffer

  try {
    data = await fs.readFile(tempFilePath)
  } catch {
    throw new APIError('Staged upload was not found.', 400)
  }

  if (data.length !== upload.filesize) {
    await fs.rm(tempFilePath, { force: true })
    throw new APIError('Staged upload is incomplete.', 400)
  }

  await fs.rm(tempFilePath, { force: true })

  return {
    name: upload.filename,
    data,
    mimetype: upload.mimeType,
    size: data.length,
  }
}

/**
 * Staged uploads hold a file temporarily until a document create or update uses it. The signed
 * upload ID remembers which file and user the upload belongs to.
 */
const tokenExpiration = 60 * 60

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Re-stage the file: generate new upload instructions, re-PUT the full byte stream, then consume.
  2. Check client-side for upload aborts -- increase timeouts and add retry on the PUT step specifically.
  3. Verify no intermediary truncates the body (nginx proxy_read_timeout, CDN body limits).
  4. Ensure the temp directory has enough disk space for the full file.

Example fix

// before
const instr = await generateStagedUploadInstructions({ filesize: file.size, ... })
await fetch(instr.request.url, { method: 'PUT', body: streamThatMayAbort })
await createWithUpload(instr) // partial file on disk -> incomplete

// after
const instr = await generateStagedUploadInstructions({ filesize: file.size, ... })
const res = await fetch(instr.request.url, { method: 'PUT', body: fullBuffer })
if (!res.ok) throw new Error('PUT failed, re-stage before retrying')
await createWithUpload(instr)
Defensive patterns

Strategy: try-catch

Validate before calling

// After PUT, verify the response is 204 before consuming
const res = await fetch(uploadUrl, { method: 'PUT', body: buf })
if (res.status !== 204) {
  throw new Error('PUT did not complete successfully -- do not consume')
}

Try / catch

try {
  await payload.create({ collection, data })
} catch (e) {
  if (e instanceof APIError && e.message === 'Staged upload is incomplete.') {
    // re-stage and re-upload the full file
  } else throw e
}

Prevention

When it happens

Trigger: The PUT to `/upload-instructions/:uploadId` was interrupted mid-stream (client disconnect, network drop) so the `.part` file was renamed or a prior partial write persisted; or a crash happened between writing and the final size check.

Common situations: Client disconnected or timed out during the chunked PUT; a proxy closed the connection early; the server crashed mid-upload and on restart the temp file is stale; clock or filesystem corruption caused a truncated write.

Related errors


AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12). Data as JSON: /api/errors/81c3c0f51977bbef. Report an issue: GitHub.