Supabase (PostgREST) rejects any POST/PATCH/PUT request that transports a body without so much as a Content-Type header. The 405/PGRST101 response is PostgREST telling you it has no idea how to deserialize the payload, so the mutation never reaches the database. Setting the right media type fixes the error instantly.
PostgREST runs in front of every Supabase REST request and validates the HTTP verb, query string, headers, and body before it hits PostgreSQL. The "Missing Content-Type header" variant of PGRST101 surfaces when PostgREST sees a request body but cannot determine the media type because the request failed to send any `Content-Type` header. Without that header, PostgREST cannot choose a media type handler to parse JSON, URL-encoded, or multipart data, so it responds with HTTP 405 and rejects the operation as if you're calling an unsupported verb. The group-1 error table in the PostgREST docs shows that these validations live in the same guardrail as malformed queries and invalid headers, which is why PGRST101 is triggered before any SQL runs.
Inspect the HTTP exchange with curl, a browser devtool, or a network proxy. The request must include the Content-Type that matches the payload.
curl -v -X POST https://your-project.supabase.co/rest/v1/widgets -H "apikey: YOUR_ANON_KEY" -H "Authorization: Bearer YOUR_JWT" -H "Content-Type: application/json" -d '{"name":"test"}'If the Content-Type line is absent, PostgREST raises PGRST101. Re-run curl with the header and you should get a 201/200 response instead of 405.
When you call Supabase via fetch, axios, or another HTTP client, always add the header before calling JSON.stringify on the payload.
const response = await fetch('https://your-project.supabase.co/rest/v1/widgets', {
method: 'POST',
headers: {
apikey: process.env.SUPABASE_ANON_KEY,
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json', // required
},
body: JSON.stringify({ name: 'test' }),
});If you build headers by merging objects, make sure you don’t accidentally remove Content-Type. Supabase?s JS client already does this for you when you call .insert(), but custom fetch wrappers still need the header.
Browsers set the Content-Type (with the boundary) automatically when you pass a FormData instance. Do not manually override it with a string such as 'multipart/form-data', because that strips the boundary and can trigger the same error. Instead, only append Content-Type when you control the media type:
const form = new FormData();
form.append('file', fileInput.files[0]);
await fetch('https://your-project.supabase.co/rest/v1/uploads', {
method: 'POST',
headers: {
apikey: SUPABASE_KEY,
Authorization: `Bearer ${token}`,
// DO NOT set Content-Type here when using FormData
},
body: form,
});If you need to send JSON, make sure you call JSON.stringify and set Content-Type: application/json explicitly.
If you front Supabase with Cloudflare, Vercel, or your own API gateway, confirm those services do not drop the Content-Type header. Inspect the raw request reaching Supabase or add temporary logging to the middleware:
addEventListener('fetch', (event) => {
console.log('incoming headers', Object.fromEntries(event.request.headers));
return event.respondWith(fetch(event.request));
});If the header disappears before Supabase sees it, configure the edge layer to pass through all headers or rebuild the request and re-add Content-Type before forwarding.
The official Supabase libraries set the required Content-Type automatically when you use the table helpers or service functions. Replace raw fetch calls with:
await supabase.from('widgets').insert({ name: 'test' });If you still need custom fetch logic (for example, fetching signed URLs or invoking edge functions), copy the headers that the Supabase client sends by default so that PostgREST receives the same metadata.
PostgREST’s error catalog groups missing headers under “Group 1 – Api Request” because the proxy validates HTTP semantics before hitting SQL. The same table also lists PGRST107 for invalid Content-Type values, which means that once you add Content-Type: application/json or another valid MIME type, PostgREST happily routes the request to the JSON or form parser documented in the PostgREST API reference.
email_conflict_identity_not_deletable: Cannot delete identity because of email conflict
How to fix "Cannot delete identity because of email conflict" in Supabase
mfa_challenge_expired: MFA challenge has expired
How to fix "mfa_challenge_expired: MFA challenge has expired" in Supabase
conflict: Database conflict, usually related to concurrent requests
How to fix "database conflict usually related to concurrent requests" in Supabase
phone_exists: Phone number already exists
How to fix "phone_exists" in Supabase
StorageApiError: resource_already_exists
StorageApiError: Resource already exists