payloadcms/payload · error · APIError
Content-Type expected to be application/json
Error message
Content-Type expected to be application/json
What it means
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`).
Source
Thrown at packages/payload/src/uploads/endpoints/uploadInstructions.ts:92
const collectionPermissions = (await getAccessResults({ req })).collections?.[
upload.collectionSlug
]
if (!collectionPermissions?.create && !collectionPermissions?.update) {
throw new Forbidden(req.t)
}
}
return uploadInstructions
? uploadInstructions.generate({ ...upload, overrideAccess, req })
: generateStagedUploadInstructions({ ...upload, req })
}
export const uploadInstructionsEndpoint: Endpoint = {
handler: async (req) => {
if (!req.json) {
throw new APIError('Content-Type expected to be application/json', 400)
}
const upload: unknown = await req.json()
if (!isUploadInstructionsRequest(upload)) {
throw new APIError('Invalid upload instructions request', 400)
}
return Response.json(await getUploadInstructions({ ...upload, req }))
},
method: 'post',
path: '/upload-instructions',
}
/**
* Stores or removes temporary files when no adapter-specific upload instructions are available.
* PUT keeps the file until it is used in a document request.
* DELETE removes a file the client no longer needs.
*/View on GitHub (pinned to 00c58b35c0)
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) })`.
Example fix
// before
await fetch(`${url}/api/upload-instructions`, {
method: 'POST',
// missing Content-Type, body is a Blob
body: new Blob([JSON.stringify(payload)]),
})
// after
await fetch(`${url}/api/upload-instructions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}) Defensive patterns
Strategy: validation
Validate before calling
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (headers['Content-Type'] !== 'application/json') {
throw new Error('upload-instructions endpoint requires application/json')
}
await fetch(`${url}/api/upload-instructions`, { method: 'POST', headers, body: JSON.stringify(payload) }) Type guard
function isJsonContentType(headers: Record<string, string>): boolean {
const ct = headers['Content-Type'] ?? headers['content-type'] ?? ''
return ct.toLowerCase().includes('application/json')
}
if (!isJsonContentType(headers)) {
headers['Content-Type'] = 'application/json'
} Try / catch
try {
const res = await fetch(`${url}/api/upload-instructions`, { method: 'POST', headers, body })
if (res.status === 400) {
const { message } = await res.json().catch(() => ({}))
if (/application\/json/i.test(message ?? '')) {
headers['Content-Type'] = 'application/json'
// retry with corrected header
}
}
} catch (err) { /* network */ } Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- Field ${field.label} has invalid relationship '${relationshi
- File type '${file.mimetype}' is not allowed.
- Could not read uploaded file for validation.
- Could not read uploaded file for type detection.
- File type ${mimeTypeFromExtension} (from extension ${typeFro
AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12).
Data as JSON: /api/errors/5da797415385c3e9.
Report an issue: GitHub.