payloadcms/payload · error · APIError

Staged upload not found. Complete the upload action first, o

Error message

Staged upload not found. Complete the upload action first, or use base64 for small local files.

What it means

The MCP file-input resolver throws this 400 when `source: 'uploadReference'` is used but `getFileFromUploadInstructions` cannot locate a staged upload matching the supplied `file.uploadReference`. It is a deliberate re-wrap of the internal 'Staged upload was not found.' error so the caller knows the two-step upload flow was not completed.

Source

Thrown at packages/plugin-mcp/src/mcp/builtin/collections/fileInput.ts:61

export async function resolveFile({
  collectionSlug,
  input,
  req,
}: {
  collectionSlug: CollectionSlug
  input?: FileInput
  req: PayloadRequest
}): Promise<File | undefined> {
  if (!input) {
    return undefined
  }

  if (input.source === 'uploadReference') {
    try {
      return await getFileFromUploadInstructions({ collectionSlug, file: input.file, req })
    } catch (error) {
      if (error instanceof Error && error.message === 'Staged upload was not found.') {
        throw new APIError(
          'Staged upload not found. Complete the upload action first, or use base64 for small local files.',
          400,
        )
      }
      throw error
    }
  }

  const uploadConfig = req.payload.collections[collectionSlug]?.config.upload

  if (!uploadConfig) {
    throw new APIError(`Collection "${collectionSlug}" does not support file uploads.`, 400)
  }

  const maxFileSize = req.payload.config.upload.limits?.fileSize
  let file: File

  if (input.source === 'base64') {

View on GitHub (pinned to 00c58b35c0)

Solutions

  1. Complete the dispatch upload (PUT the bytes to the signed URL returned by `generateUploadInstructions`) before calling the MCP tool with that `uploadReference`
  2. For small local files, switch to `source: 'base64'` which is single-step
  3. Regenerate upload instructions if the signed URL has expired and obtain a fresh `uploadReference`
  4. Verify the `uploadReference.prefix` matches the collection the tool is operating on

Example fix

// before — uploadReference supplied before the dispatch PUT
{ source: 'uploadReference', file: { filename, mimeType, size, uploadReference: { prefix } } }
// after — small file via base64, single step
{ source: 'base64', name: 'logo.png', mimeType: 'image/png', data: base64String }
Defensive patterns

Strategy: validation

Validate before calling

// Verify the dispatch PUT succeeded before passing an uploadReference
async function ensureDispatched(signedUrl: string, bytes: Buffer) {
  const res = await fetch(signedUrl, { method: 'PUT', body: bytes })
  if (!res.ok) throw new Error(`dispatch upload failed: ${res.status}`)
}

Try / catch

import { APIError } from 'payload'
try {
  await tool.call({ source: 'uploadReference', file })
} catch (e) {
  if (e instanceof APIError && e.statusCode === 400 && /Staged upload not found/.test(e.message)) {
    // fall back to base64 for small files
    return tool.call({ source: 'base64', name: file.filename, mimeType: file.mimeType, data: bytes.toString('base64') })
  }
  throw e
}

Prevention

When it happens

Trigger: Calling an MCP upload tool with `source: 'uploadReference'` before the dispatch PUT to the signed URL has finished; passing an `uploadReference` whose staged prefix was already consumed or whose SAS token expired; passing an uploadReference produced for collection A to a tool operating on collection B.

Common situations: Forgetting the dispatch step after `generateUploadInstructions`; calling the create/update MCP tool twice with the same reference (second call finds nothing); Azure SAS token older than 3 hours; cross-collection upload-reference reuse.

Related errors


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