Content-Type: application/json — Why It Fails and How to Fix It
Quick answer
💡Set the Content-Type header to application/json on every request that sends a JSON body. In fetch, add headers: { 'Content-Type': 'application/json' } and wrap the body with JSON.stringify. In Express, register app.use(express.json()) before your route handlers so the middleware parses the body. A missing header causes 415 errors or an undefined req.body.
Error symptoms
- ✕
HTTP 415 Unsupported Media Type response from the server - ✕
req.body is undefined or an empty object in Express even though the client sent data - ✕
Server receives a raw string instead of a parsed JavaScript object - ✕
curl request sends data but the server rejects it or cannot read any fields - ✕
API gateway returns 400 Bad Request with a message about missing or invalid media type - ✕
CORS preflight OPTIONS request appears unexpectedly in browser network tools
Common causes
- •Calling fetch with a JSON body but forgetting to add the Content-Type header
- •Using curl with --data or -d without -H 'Content-Type: application/json', causing it to send application/x-www-form-urlencoded
- •Not registering express.json() middleware, so Express never parses the request body
- •Registering express.json() after the route that needs it, making the middleware run too late
- •Sending the body as a plain string rather than the result of JSON.stringify, so the JSON is malformed
- •A proxy or API gateway stripping the Content-Type header before it reaches the application server
When it happens
- •Building a REST API client in the browser using the Fetch API for the first time
- •Writing curl commands to test an API endpoint that requires a JSON request body
- •Migrating from body-parser to the built-in express.json() middleware in Express 4.16+
- •Receiving webhook payloads and finding that the parsed body is empty despite the payload being visible in raw logs
Examples and fixes
A browser application submits a user login form by posting credentials to an API. The wrong version omits the Content-Type header, so the server receives the body but cannot identify the media type.
Sending a JSON request body with the Fetch API
❌ Wrong
async function loginUser(email, password) {
const requestBody = { email, password };
const response = await fetch('/api/auth/login', {
method: 'POST',
// Content-Type header is missing
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Login failed: ${errorText}`);
}
return response.json();
}✅ Fixed
async function loginUser(email, password) {
const requestBody = { email, password };
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Login failed ${response.status}: ${errorText}`);
}
return response.json();
}Without the Content-Type header, the Fetch API sends the body as a plain string. The server's JSON parsing middleware checks the header to decide whether to parse the body, and if the header is absent or set to text/plain, it skips parsing entirely. Express responds with a 415 status or leaves req.body undefined. Adding the header tells the server to parse the body as JSON.
An Express application defines a POST route to create a new product. The wrong version registers the body parser after the route, so the route handler runs before the body is parsed.
Registering Express JSON middleware in the correct order
❌ Wrong
const express = require('express');
const app = express();
// Route is registered BEFORE the middleware
app.post('/api/products', (req, res) => {
const { name, price, category } = req.body; // undefined!
if (!name || !price) {
return res.status(400).json({ error: 'Missing required fields' });
}
res.status(201).json({ id: Date.now(), name, price, category });
});
// Middleware registered too late
app.use(express.json());
app.listen(3000);✅ Fixed
const express = require('express');
const app = express();
// Middleware registered BEFORE all routes
app.use(express.json());
app.post('/api/products', (req, res) => {
const { name, price, category } = req.body;
if (!name || price === undefined) {
return res.status(400).json({ error: 'name and price are required' });
}
res.status(201).json({
id: Date.now(),
name,
price: Number(price),
category: category || 'uncategorized'
});
});
app.listen(3000, () => console.log('Listening on port 3000'));Express middleware runs in the order it is registered. When a route is registered before express.json(), the route handler executes first and finds req.body is undefined because no middleware has parsed the body yet. Moving app.use(express.json()) before all route definitions ensures the body is available in every handler. A 400 response with a clear message is returned when required fields are still missing after parsing.
Content-Type tells middleware how to parse the body
The Content-Type HTTP header serves as a declaration of the media type and encoding of the request body. When a client sends a POST or PUT request with a body, the Content-Type header tells the server which format to expect. The server's parsing layer reads this header before touching the body and decides which parser to invoke. If the header says application/json, the server invokes its JSON parser. If the header says application/x-www-form-urlencoded, the server invokes its URL-encoded form parser. If the header is absent or set to a value the server does not recognize, the server typically responds with 415 Unsupported Media Type or leaves the body unparsed.
In Express, the express.json() middleware function is responsible for JSON body parsing. It inspects each incoming request's Content-Type header, and if the value is application/json, it reads the request body stream, passes it through JSON.parse, and assigns the result to req.body. If the Content-Type is anything else, express.json() passes the request to the next middleware without touching req.body. This means that a request with the correct JSON body but the wrong or missing Content-Type header will arrive at your route handler with req.body set to undefined or an empty object, depending on whether another body parser ran first.
The HTTP/1.1 specification defines Content-Type as describing the media type of the representation sent in the message payload. For JSON, the registered media type is application/json as defined in RFC 8259. An optional charset parameter can follow, such as application/json; charset=utf-8, but RFC 8259 mandates UTF-8 as the default encoding for JSON, so the charset parameter is rarely necessary and many APIs ignore it entirely. Servers that are strict about charset validation may reject application/json; charset=utf-16 with a 415 error even though the JSON itself is valid.
Content-Type negotiation happens at the protocol level before your application code runs. This is why the fix for a missing header cannot be applied inside the route handler. The route handler receives an already-parsed (or not-parsed) req.body. Inspecting or modifying the request body inside the handler does not help if the middleware layer never parsed it.
Diagnosing 415 and missing body in Express
When req.body is undefined in an Express route handler, the first step is to confirm that express.json() is registered at all and that it appears before the route in the middleware stack. A common mistake is to define routes at the top of the file and call app.use(express.json()) lower in the file. Express processes middleware in declaration order, so the route executes before the body parser has a chance to run.
The second diagnostic step is to inspect the Content-Type header of the incoming request. Add a temporary middleware that logs req.headers['content-type'] for incoming requests. If the header is absent, text/plain, or application/x-www-form-urlencoded, the body parser will not parse a JSON body and req.body will be empty. If the header is application/json but req.body is still empty, check whether the body itself is valid JSON by logging req.rawBody or by using the raw request body middleware to capture the stream before parsing.
For 415 Unsupported Media Type responses, the server is explicitly rejecting the request because the media type is not acceptable. This means the server has body parsing configured with strict media type checking. In Express, express.json() returns a 415 when the Content-Type is not application/json and the request method is POST or PUT. Some API frameworks and gateways also produce 415 when the Content-Type header contains the correct media type but has additional parameters they do not recognize.
Using curl to isolate the problem is effective. Reproduce the same request that fails from the browser using a curl command with explicit headers. If the curl request succeeds, the problem is in how the browser client is constructing the request. If the curl request also fails, the problem is on the server side. Compare the exact headers sent by each client using curl's -v flag to view request and response headers, and compare them against the headers visible in the browser's network panel.
Setting Content-Type in fetch and curl
The Fetch API does not automatically set Content-Type when you provide a body as a string. You must add it explicitly in the headers object. The pattern for a JSON POST request is to add a headers object with Content-Type set to application/json, pass the JavaScript object through JSON.stringify before assigning it to body, and check the response status before calling res.json(). Checking the status is important because calling res.json() on a non-JSON response body, such as an HTML error page returned by a proxy, throws a SyntaxError.
For curl, the default behavior when using -d or --data is to send the body with Content-Type: application/x-www-form-urlencoded. To send JSON, you must add the -H flag with the Content-Type header explicitly: curl -X POST -H 'Content-Type: application/json' -d '{...}' https://api.example.com/endpoint. The order of the flags does not matter, but omitting -H causes the API to receive form-encoded data instead of JSON. Use -v to confirm the header appears in the Request Headers section of the output.
In the Axios HTTP client for Node.js, the behavior is different: Axios automatically sets Content-Type to application/json when the data option is a JavaScript object. However, if you pass a pre-stringified JSON string as the data, Axios does not automatically add the header and you must set it manually. When using the Axios request config with data as a plain object, rely on the automatic header. When using a string body for any reason, add the header explicitly.
For server-to-server requests in Node.js using the native http module or https module, you must set Content-Type in the options object and also set Content-Length to the byte length of the body string. Forgetting Content-Length causes many servers to wait for more data and eventually time out. The safer approach in Node.js server code is to use a higher-level HTTP client such as Axios, got, or node-fetch, all of which handle these low-level details automatically when given a plain JavaScript object.
Content-Type versus Accept header purposes
Content-Type and Accept are both HTTP headers related to media types, but they communicate in opposite directions and serve different purposes. Content-Type describes the media type of the message body that the sender is providing. Accept describes the media types that the sender can understand in a response. Confusing the two is a common source of incorrect behavior, particularly when an API client sends Accept: application/json but omits Content-Type: application/json on a request with a JSON body.
When a client sends a GET request, there is no request body and therefore no need for Content-Type. The Accept header tells the server that the client wants JSON in the response. The server reads Accept and responds with Content-Type: application/json in the response headers. This is content negotiation: the client expresses its preferences and the server selects the best matching format.
When a client sends a POST or PUT request with a JSON body, it needs both headers for different reasons. Content-Type: application/json on the request tells the server how to parse the body. Accept: application/json on the request tells the server what format the client wants for the response body. A client that sends Content-Type but not Accept will get the server's default response format, which may or may not be JSON depending on the API. A client that sends Accept but not Content-Type may have its request body rejected.
A related header is Content-Encoding, which describes the transfer encoding of the body such as gzip or deflate. This is different from Content-Type. An application/json body that has been gzip-compressed would use both Content-Type: application/json and Content-Encoding: gzip. The server must decompress the body before parsing it as JSON. Most HTTP clients handle compression transparently, but it is useful to understand the distinction when debugging requests that fail with malformed body errors even though the Content-Type is correct.
When application/json triggers CORS preflight
The CORS specification divides cross-origin requests into two categories: simple requests and preflighted requests. Simple requests are sent directly without a preceding OPTIONS preflight. Preflighted requests cause the browser to send an OPTIONS request first to verify that the server allows the cross-origin request. Setting Content-Type to application/json automatically makes a request a preflighted request, which means an OPTIONS preflight request will appear in your browser's network panel before the actual POST.
A request qualifies as a simple request only if the Content-Type is one of three values: application/x-www-form-urlencoded, multipart/form-data, or text/plain. Using application/json does not qualify as a simple request because it is not in that list. This means that any API that accepts JSON bodies from browsers will receive preflight OPTIONS requests. The server must respond to OPTIONS requests with the appropriate CORS headers, including Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers. If Access-Control-Allow-Headers does not include Content-Type, the browser blocks the actual request.
This is a common confusion for developers who successfully send text/plain or form-encoded requests to a cross-origin API and then switch to JSON bodies. The existing CORS configuration may not include Content-Type in the allowed headers, causing the preflight to fail with a CORS error that mentions a missing Access-Control-Allow-Headers value. The fix is to add Content-Type to the server's CORS configuration, not to change the Content-Type of the request.
In Express, the cors package handles preflight automatically when configured correctly. Calling app.use(cors({ origin: '...', allowedHeaders: ['Content-Type', 'Authorization'] })) ensures that both the preflight OPTIONS response and the actual response include the correct CORS headers. Without explicitly listing allowedHeaders, some CORS middleware implementations may not include Content-Type in the Access-Control-Allow-Headers response header, which causes preflight failures for any request that uses application/json.
Standardizing Content-Type across API clients
Building a consistent API client layer eliminates the entire class of Content-Type errors by centralizing header management. Rather than adding Content-Type and Accept headers to every individual fetch call, define a base request function that adds these headers automatically and handles common error cases. This base function should also handle JSON serialization of the request body and JSON deserialization of the response body, throwing typed errors when either step fails.
For TypeScript applications, defining an api function that accepts a typed request configuration and returns a typed response creates a contract that is enforced at compile time. The implementation can use generic type parameters to annotate the expected response body type. This pattern means that every API call in the codebase uses the same headers without any developer needing to remember to add Content-Type manually.
API clients built on Axios benefit from instance-level default headers. Creating an Axios instance with axios.create({ baseURL: '...', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }) applies those headers to every request made through that instance. Request interceptors can add authentication headers dynamically. Response interceptors can normalize error responses into a consistent format. This approach is particularly effective in larger codebases where many developers make API calls, because the correct headers are applied without any individual action.
On the server side, documenting expected Content-Type in your API with an OpenAPI specification creates a machine-readable contract that tools can validate against. Middleware that validates the Content-Type header and returns 415 with a clear error message before the request reaches route handlers provides early feedback to API clients about incorrect usage. Logging the Content-Type value alongside the request method and path in your access logs makes it easy to retrospectively analyze which clients are sending incorrect headers.
Quick fix checklist
- ✓Add headers: { 'Content-Type': 'application/json' } to every fetch call with a JSON body
- ✓Wrap the request body with JSON.stringify before assigning it to the body option in fetch
- ✓Register app.use(express.json()) before all route definitions in Express
- ✓Use -H 'Content-Type: application/json' in curl commands that send JSON data
- ✓Confirm the CORS configuration includes Content-Type in Access-Control-Allow-Headers
- ✓Check for proxies or gateways that may be stripping or overwriting Content-Type headers
- ✓Add Accept: application/json as well as Content-Type to receive JSON responses from content-negotiating APIs
- ✓Verify the request body is valid JSON by using a validator before sending
Related guides
Frequently asked questions
Why does my Express route have req.body as undefined?
Express does not parse request bodies automatically. You must register the express.json() middleware with app.use(express.json()) before any route that needs to read req.body. If the middleware is registered after the route definition, it runs too late and req.body is undefined in the route handler.
What is the 415 Unsupported Media Type error?
A 415 status means the server received a Content-Type header value it does not support for the requested endpoint. When an API requires application/json but the client sends application/x-www-form-urlencoded or omits Content-Type entirely, many servers respond with 415. Fix it by adding Content-Type: application/json to the request headers.
Does curl send JSON automatically when I use -d?
No. The -d flag sends the body with Content-Type: application/x-www-form-urlencoded by default. To send JSON, you must add the -H 'Content-Type: application/json' flag explicitly. Without this flag, the server receives URL-encoded data rather than JSON, even if the body string looks like JSON.
Does Axios automatically add Content-Type for JSON?
Yes, when you pass a plain JavaScript object as the data option, Axios automatically serializes it with JSON.stringify and sets Content-Type: application/json. However, if you pass a pre-stringified string, Axios does not add the header automatically and you must set it yourself in the request config.
Why does adding Content-Type: application/json trigger a CORS preflight?
The CORS specification defines simple requests as those using only application/x-www-form-urlencoded, multipart/form-data, or text/plain as the Content-Type. Using application/json falls outside that list, which causes the browser to send a preflight OPTIONS request before the actual POST. The server must respond with Access-Control-Allow-Headers: Content-Type in the preflight response, otherwise the browser blocks the request entirely before it reaches your application code.
What is the difference between Content-Type and Accept headers?
Content-Type describes the media type of the body you are sending to the server. Accept describes the media types you are willing to receive in the response. For a JSON API, set Content-Type: application/json on requests with a body, and set Accept: application/json on all requests to indicate you want JSON responses.
Do I need charset=utf-8 in the Content-Type header?
No. RFC 8259 specifies that JSON must be encoded in UTF-8 by default. Sending Content-Type: application/json is sufficient and the charset parameter is optional. Most servers ignore it. Some strict servers may reject a request with charset=utf-16 if they only support UTF-8, but that is an edge case.
Can a proxy strip the Content-Type header?
Yes. Some proxies, API gateways, and load balancers modify or strip headers. If a request works when sent directly to the server but fails when routed through a proxy, check whether the proxy is altering the Content-Type header. Use curl with -v against both the proxy URL and the direct server URL and compare the request headers.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-05.