{"record":{"id":"5da797415385c3e9","repo":"payloadcms/payload","slug":"content-type-expected-to-be-application-json","errorCode":null,"errorMessage":"Content-Type expected to be application/json","messagePattern":"Content-Type expected to be application/json","errorType":"http","errorClass":"APIError","httpStatus":400,"severity":"error","filePath":"packages/payload/src/uploads/endpoints/uploadInstructions.ts","lineNumber":92,"sourceCode":"\n    const collectionPermissions = (await getAccessResults({ req })).collections?.[\n      upload.collectionSlug\n    ]\n\n    if (!collectionPermissions?.create && !collectionPermissions?.update) {\n      throw new Forbidden(req.t)\n    }\n  }\n\n  return uploadInstructions\n    ? uploadInstructions.generate({ ...upload, overrideAccess, req })\n    : generateStagedUploadInstructions({ ...upload, req })\n}\n\nexport const uploadInstructionsEndpoint: Endpoint = {\n  handler: async (req) => {\n    if (!req.json) {\n      throw new APIError('Content-Type expected to be application/json', 400)\n    }\n\n    const upload: unknown = await req.json()\n    if (!isUploadInstructionsRequest(upload)) {\n      throw new APIError('Invalid upload instructions request', 400)\n    }\n\n    return Response.json(await getUploadInstructions({ ...upload, req }))\n  },\n  method: 'post',\n  path: '/upload-instructions',\n}\n\n/**\n * Stores or removes temporary files when no adapter-specific upload instructions are available.\n * PUT keeps the file until it is used in a document request.\n * DELETE removes a file the client no longer needs.\n */","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/payloadcms/payload/blob/00c58b35c0ed348ddc22daabf467b139727214fd/packages/payload/src/uploads/endpoints/uploadInstructions.ts#L74-L110","documentation":"The `POST /upload-instructions` endpoint reads the body via `req.json()`. Before doing so it asserts that `req.json` is truthy — a property the request has only when the framework detected a JSON-parseable body (i.e. a JSON-compatible `Content-Type`). If absent, the endpoint rejects with HTTP 400 rather than letting `req.json()` throw an opaque parse error. This guards all four downstream fields (`collectionSlug`, `filename`, `filesize`, `mimeType`).","triggerScenarios":"Calling `POST /api/upload-instructions` with a missing or non-JSON `Content-Type` header (e.g. `multipart/form-data`, `text/plain`, `application/x-www-form-urlencoded`, or no header at all).","commonSituations":"A client built for the legacy multipart upload flow POSTs to the new instructions endpoint. A proxy/CDN rewrites or strips the `Content-Type`. A fetch call forgets to set the header, defaulting the body to a Blob/FormData. Cross-origin preflight issues cause the header to be dropped.","solutions":["Set `Content-Type: application/json` on the request and send a stringified JSON body.","Ensure no middleware/proxy strips or rewrites the header before it reaches Payload.","If uploading bytes directly, use the staged `PUT /upload-instructions/:uploadId` route (raw body) instead of the JSON instructions route.","Re-derive the request with `fetch(url, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(payload) })`."],"exampleFix":"// before\nawait fetch(`${url}/api/upload-instructions`, {\n  method: 'POST',\n  // missing Content-Type, body is a Blob\n  body: new Blob([JSON.stringify(payload)]),\n})\n\n// after\nawait fetch(`${url}/api/upload-instructions`, {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify(payload),\n})","handlingStrategy":"validation","validationCode":"const headers: Record<string, string> = { 'Content-Type': 'application/json' }\nif (headers['Content-Type'] !== 'application/json') {\n  throw new Error('upload-instructions endpoint requires application/json')\n}\nawait fetch(`${url}/api/upload-instructions`, { method: 'POST', headers, body: JSON.stringify(payload) })","typeGuard":"function isJsonContentType(headers: Record<string, string>): boolean {\n  const ct = headers['Content-Type'] ?? headers['content-type'] ?? ''\n  return ct.toLowerCase().includes('application/json')\n}\nif (!isJsonContentType(headers)) {\n  headers['Content-Type'] = 'application/json'\n}","tryCatchPattern":"try {\n  const res = await fetch(`${url}/api/upload-instructions`, { method: 'POST', headers, body })\n  if (res.status === 400) {\n    const { message } = await res.json().catch(() => ({}))\n    if (/application\\/json/i.test(message ?? '')) {\n      headers['Content-Type'] = 'application/json'\n      // retry with corrected header\n    }\n  }\n} catch (err) { /* network */ }","preventionTips":["Always set `Content-Type: application/json` when calling JSON endpoints.","For direct byte uploads use the staged PUT route, not the JSON instructions route.","Ensure proxies do not rewrite the Content-Type."],"tags":["upload","http","content-type","validation"],"backgroundTag":null,"analyzedSha":"00c58b35c0ed348ddc22daabf467b139727214fd","analyzedAt":"2026-08-12T20:45:03.758Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}