payloadcms/payload · error · APIError

Uploaded file is larger than expected.

Error message

Uploaded file is larger than expected.

What it means

The staged `PUT /upload-instructions/:uploadId` streams the request body to disk, accumulating `uploadedSize` per chunk. If at any point `uploadedSize` exceeds the `filesize` declared when the upload ID was minted (signed JWT containing `filesize`), Payload throws `APIError` HTTP 400 `Uploaded file is larger than expected.` The partial `.part` file is cleaned up in the surrounding `catch`.

Source

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

  const uploadPath = path.join(directory, upload.id)
  const temporaryPath = `${uploadPath}.${randomUUID()}.part`
  const file = await fs.open(temporaryPath, 'wx')
  let uploadedSize = 0

  try {
    try {
      const reader = req.body?.getReader()

      while (reader) {
        const { done, value } = await reader.read()

        if (done) {
          break
        }

        uploadedSize += value.byteLength
        if (uploadedSize > upload.filesize) {
          throw new APIError('Uploaded file is larger than expected.', 400)
        }

        let offset = 0
        while (offset < value.byteLength) {
          const { bytesWritten } = await file.write(value, offset)
          offset += bytesWritten
        }
      }

      if (uploadedSize !== upload.filesize) {
        throw new APIError('Uploaded file size does not match the expected size.', 400)
      }
    } finally {
      await file.close()
    }

    const collection = req.payload.collections[upload.collectionSlug]!.config
    await checkFileRestrictions({

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Request fresh upload instructions (`POST /upload-instructions`) with the exact byte length of the file you will stream, then PUT exactly that many bytes.
  2. Compute `filesize` from the actual `File`/`Blob`/buffer you upload (`file.size`), not a metadata estimate.
  3. Do not reuse an uploadId across different files or after the file changes.
  4. Ensure the HTTP client doesn't transform the body (extra framing, appended trailers).
  5. If resumable uploads are needed, re-mint the uploadId rather than appending to a used one.

Example fix

// before — filesize mismatch
const { file } = await getUploadInstructions({
  collectionSlug: 'media', filename: 'a.png', filesize: 1024, mimeType: 'image/png', req, // wrong size
})
await fetch(file.request.url, { method: 'PUT', body: realBlob /* 2048 bytes */ })

// after — declare the true size
const realSize = realBlob.size
const { file } = await getUploadInstructions({
  collectionSlug: 'media', filename: 'a.png', filesize: realSize, mimeType: 'image/png', req,
})
await fetch(file.request.url, { method: 'PUT', body: realBlob })
Defensive patterns

Strategy: validation

Validate before calling

const realSize = file.size // or Buffer.byteLength(buffer)
const { file: instr } = await getUploadInstructions({
  collectionSlug: 'media', filename, filesize: Math.trunc(realSize), mimeType, req,
})
if (realSize !== Math.trunc(realSize)) {
  throw new Error('filesize must be a non-negative safe integer matching the stream length')
}

Type guard

function sizesMatch(declared: number, actual: number): boolean {
  return Number.isSafeInteger(declared) && declared >= 0 && actual <= declared
}
if (!sizesMatch(instr.size, blob.size)) {
  // re-request instructions with the correct size
}

Try / catch

try {
  const res = await fetch(instr.request.url, { method: 'PUT', body: blob })
  if (res.status === 400) {
    const { message } = await res.json().catch(() => ({}))
    if (/larger than expected/i.test(message ?? '')) {
      // re-mint instructions with blob.size and PUT again
    }
  }
} catch (err) { /* network */ }

Prevention

When it happens

Trigger: `PUT /api/upload-instructions/:uploadId` where the total streamed byte count surpasses the `filesize` baked into the signed uploadId token. The client sent more bytes than it declared in the prior `POST /upload-instructions`.

Common situations: The client computed `filesize` from a different (smaller) artifact than the one it actually streams (e.g. pre- vs post-compression, or a different file). A retry/resume appended to an existing stream. A proxy or middleware added bytes/chunked encoding unexpectedly. The client reused a stale uploadId for a larger file. Concurrency: two PUTs to the same id.

Related errors


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