{"record":{"id":"60cd3cf00d301746","repo":"payloadcms/payload","slug":"invalid-or-expired-staged-upload","errorCode":null,"errorMessage":"Invalid or expired staged upload.","messagePattern":"Invalid or expired staged upload\\.","errorType":"exception","errorClass":"APIError","httpStatus":400,"severity":"error","filePath":"packages/payload/src/uploads/stagedUpload.ts","lineNumber":269,"sourceCode":"    const secret = new TextEncoder().encode(req.payload.secret)\n    const { payload } = await jwtVerify<StagedUploadToken>(uploadID, secret)\n\n    if (\n      typeof payload.id !== 'string' ||\n      !/^[\\da-f-]{36}$/i.test(payload.id) ||\n      typeof payload.collectionSlug !== 'string' ||\n      typeof payload.filename !== 'string' ||\n      !Number.isSafeInteger(payload.filesize) ||\n      payload.filesize < 0 ||\n      typeof payload.mimeType !== 'string' ||\n      (payload.user !== null && typeof payload.user !== 'string')\n    ) {\n      throw new Error()\n    }\n\n    return payload as StagedUploadToken\n  } catch {\n    throw new APIError('Invalid or expired staged upload.', 400)\n  }\n}\n","sourceCodeStart":251,"sourceCodeEnd":272,"githubUrl":"https://github.com/payloadcms/payload/blob/00c58b35c0ed348ddc22daabf467b139727214fd/packages/payload/src/uploads/stagedUpload.ts#L251-L272","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before -- secret mismatch between staging server and consuming server\n// server A: PAYLOAD_SECRET=alpha  -> generates uploadId\n// server B: PAYLOAD_SECRET=beta   -> verifies uploadId -> error\n\n// after\n// docker-compose / env: set the same secret everywhere\nPAYLOAD_SECRET=shared-secret-value","handlingStrategy":"validation","validationCode":"// Ensure PAYLOAD_SECRET is consistent and the flow completes within 1 hour\nconst elapsed = Date.now() - instructionsGeneratedAt\nif (elapsed > 60 * 60 * 1000) {\n  throw new Error('Upload instructions expired -- regenerate')\n}\nif (!process.env.PAYLOAD_SECRET) throw new Error('PAYLOAD_SECRET not set')","typeGuard":null,"tryCatchPattern":"try {\n  await verifyUploadId(uploadId)\n} catch (e) {\n  if (e instanceof APIError && e.message === 'Invalid or expired staged upload.') {\n    // regenerate instructions with current secret\n  } else throw e\n}","preventionTips":["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."],"tags":["upload","staged-upload","jwt","expired","secret"],"backgroundTag":null,"analyzedSha":"00c58b35c0ed348ddc22daabf467b139727214fd","analyzedAt":"2026-08-12T20:45:03.758Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}