payloadcms/payload · error · APIError
Invalid or expired staged upload.
Error message
Invalid or expired staged upload.
What it means
Thrown by `verifyUploadID` when the signed upload JWT cannot be verified or its payload is structurally invalid. This covers expiration (1-hour window), signature mismatch, non-string input, or missing/malformed fields (id, collectionSlug, filename, filesize, mimeType, user).
Source
Thrown at packages/payload/src/uploads/stagedUpload.ts:269
const secret = new TextEncoder().encode(req.payload.secret)
const { payload } = await jwtVerify<StagedUploadToken>(uploadID, secret)
if (
typeof payload.id !== 'string' ||
!/^[\da-f-]{36}$/i.test(payload.id) ||
typeof payload.collectionSlug !== 'string' ||
typeof payload.filename !== 'string' ||
!Number.isSafeInteger(payload.filesize) ||
payload.filesize < 0 ||
typeof payload.mimeType !== 'string' ||
(payload.user !== null && typeof payload.user !== 'string')
) {
throw new Error()
}
return payload as StagedUploadToken
} catch {
throw new APIError('Invalid or expired staged upload.', 400)
}
}
View on GitHub (pinned to 00c58b35c0)
Solutions
- Regenerate the upload instructions -- if the secret rotated or the token expired, the old uploadId is unrecoverable.
- Ensure `PAYLOAD_SECRET` (or `secret`) is identical across all instances/replicas processing uploads.
- Complete the full upload flow (instructions -> PUT -> consume) within the one-hour token window.
- Pass the uploadId exactly as received -- do not re-encode, trim, or URL-decode it.
Example fix
// before -- secret mismatch between staging server and consuming server // server A: PAYLOAD_SECRET=alpha -> generates uploadId // server B: PAYLOAD_SECRET=beta -> verifies uploadId -> error // after // docker-compose / env: set the same secret everywhere PAYLOAD_SECRET=shared-secret-value
Defensive patterns
Strategy: validation
Validate before calling
// Ensure PAYLOAD_SECRET is consistent and the flow completes within 1 hour
const elapsed = Date.now() - instructionsGeneratedAt
if (elapsed > 60 * 60 * 1000) {
throw new Error('Upload instructions expired -- regenerate')
}
if (!process.env.PAYLOAD_SECRET) throw new Error('PAYLOAD_SECRET not set') Try / catch
try {
await verifyUploadId(uploadId)
} catch (e) {
if (e instanceof APIError && e.message === 'Invalid or expired staged upload.') {
// regenerate instructions with current secret
} else throw e
} Prevention
- Set PAYLOAD_SECRET to the same value on every server instance.
- Complete the upload flow within the 1-hour JWT window.
- Do not modify or re-encode the uploadId string.
- Rotate secrets during planned downtime, not mid-upload.
When it happens
Trigger: The `uploadId` is missing, not a string, tampered with, signed with a different `payload.secret`, or expired (>1 hour old). Also thrown if any required claim inside the JWT fails the runtime type/narrowing checks.
Common situations: Server `PAYLOAD_SECRET` changed between staging and consuming (e.g. rotated key, different env); the uploadId sat in a client queue longer than one hour; the JWT was URL-decoded incorrectly; a load balancer routes staging and consuming to servers with different secrets.
Related errors
- Upload collection ${upload.collectionSlug} was not found
- You are not allowed to perform this action.
- Uploaded file is larger than expected.
- Uploaded file size does not match the expected size.
- Invalid staged upload.
AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12).
Data as JSON: /api/errors/60cd3cf00d301746.
Report an issue: GitHub.