payloadcms/payload · error · APIError

Staged upload not found. Complete the upload action first…

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.

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.

Appendix: 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)