What the header actually declares
HTTP carries bytes. Bytes on their own are meaningless — the same octets could be JSON, a JPEG or a
fragment of a CSV, and nothing in the transport distinguishes them.
Content-Type supplies that missing information: it names the media type of
the representation in the body, and the recipient uses it to choose how to interpret what follows.
Two consequences follow immediately, and both catch people out.
First, it applies to any message with a body, in either direction. It is not a request
header. A request carrying a body describes that body with Content-Type; a response carrying a
body describes that body with Content-Type. The same header name, doing the same job, about
whichever body is present in the message you are looking at. A GET request has no body, so
setting Content-Type on one declares the type of nothing — harmless, occasionally confusing,
and a habit worth dropping.
Second, it is a declaration, and the server is entitled to believe it. Send perfectly good
JSON labelled text/plain and a correct server hands it to a text handler, or refuses it.
Send malformed nonsense labelled application/json and it goes to the JSON parser, which fails
with a 400. The label decides the route; the bytes only decide
whether the route works. Servers that ignore the label and sniff the content are the exception, and in
browsers that behaviour is a documented security problem rather than a feature.
The grammar, piece by piece
A media type — the term the specifications use, and the successor to the older name “MIME type” — has exactly three structural parts.
Content-Type: application/json; charset=utf-8
^^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^
type subtype parameter
The type is the top-level category, and the registry is small and closed:
application, text, image, audio, video,
font, model, multipart, message, plus
example for documentation. You will not invent a new one.
The subtype names the specific format and is where essentially all the variety lives. Subtypes fall into registration trees signalled by a prefix, which is worth recognising when you meet an unfamiliar one:
| Tree | Form | Meaning |
|---|---|---|
| Standards | application/json | Registered with IANA through a standards process |
| Vendor | application/vnd.api+json | Defined by a specific product or organisation |
| Personal | application/prs.example | Personal or experimental, unregistered |
| Unregistered | application/x.custom | Private use, never to be registered |
Legacy x- | application/x-www-form-urlencoded | An old convention; new types should not use it, but several old ones are now permanent |
The parameters follow as ; name=value pairs and refine the type. Which
parameters are meaningful is defined by the media type itself — there is no universal set. Points of
grammar that matter in practice: type, subtype and parameter names are case-insensitive, so
Application/JSON is valid; parameter values may or may not be, depending on the
parameter; whitespace around the semicolon is permitted; and a value containing characters outside the
token set must be quoted, as in boundary="--=_Part 42". Most bugs here come from code that
compares the whole header string to "application/json" and therefore fails the moment a
client appends a legitimate parameter. Parse the header — split on ;, lowercase the type,
compare that — rather than matching the raw value.
The charset parameter
charset names the character encoding used to turn text into bytes. It is meaningful only for
textual formats, and its importance varies more than people expect:
application/json: do not send it. The media type registration defines nocharsetparameter at all, because RFC 8259 already requires JSON exchanged between systems to be UTF-8. Adding it is redundant at best, and a strict parser is within its rights to object.application/json; charset=utf-8is nonetheless extremely widespread and almost always tolerated.text/*: send it. Fortext/plain,text/htmland especiallytext/csv, the encoding is genuinely ambiguous, and getting it wrong is how a name with an accent in it becomes three replacement characters. Saycharset=utf-8explicitly and stop relying on defaults.- Binary types: meaningless. There is no character encoding for a PNG.
Note that charset is unrelated to compression. A gzipped body is still described by
Content-Type as whatever it decompresses to; the compression is announced separately in
Content-Encoding. Confusing the two produces bodies that parse as binary garbage.
The boundary parameter
For any multipart/* type, boundary is not optional — the body is
uninterpretable without it. Multipart bodies contain several independent parts concatenated together, and
the boundary is the delimiter string that separates them:
Content-Type: multipart/form-data; boundary=----X9fK2pQ7 ------X9fK2pQ7 Content-Disposition: form-data; name="title" Quarterly report ------X9fK2pQ7 Content-Disposition: form-data; name="file"; filename="q3.csv" Content-Type: text/csv id,amount 1,4200 ------X9fK2pQ7--
Each part is preceded by two hyphens plus the boundary; the final one is followed by two more hyphens.
The value must not occur anywhere in the content, which is why clients generate a long random string
rather than something readable. Two practical consequences: let your HTTP client set this
header, since hand-writing it with a boundary that does not match the body you built produces a
body the server cannot split; and note that each part has its own Content-Type, so a
multipart request describes its parts individually — which is exactly how a file upload declares
text/csv for the file while the surrounding fields stay plain text.
The three request body encodings
Nearly every request body you send is one of three types. They are not interchangeable, and choosing wrongly is the single largest source of 415 responses.
application/json
The default for APIs, and rightly so. It has real types — strings, numbers, booleans, null, arrays, nested objects — so a payload's structure survives the trip intact and can be validated against a schema.
Content-Type: application/json
{ "sku": "TEA-004", "quantity": 2, "giftWrap": false, "notes": null }
Its one real gap is binary data, which JSON cannot represent; a file must be base64-encoded, growing by about a third. If a JSON body is being rejected and you cannot see why, run it through the JSON formatter first — a trailing comma produces a 400 whose message rarely says so.
application/x-www-form-urlencoded
The default encoding of an HTML form, and the format most OAuth token endpoints require. The body is a single line of percent-encoded key–value pairs joined by ampersands — the same syntax as a query string, moved into the body:
Content-Type: application/x-www-form-urlencoded sku=TEA-004&quantity=2¬es=with%20a%20bow
Everything is a string; there is no way to express a number, a boolean, a null or nesting. Frameworks
invent conventions for structure — items[0][sku]=… and similar — but none of it is standard,
and two frameworks will disagree about how to read it. Reserved characters must be percent-encoded, which
the URL encoder will do for you, and note that in this format a space is
encoded as + rather than %20, a quirk inherited from HTML forms.
This is also the accidental default in several HTTP clients and libraries. If you pass a plain object to a
client that form-encodes by default and the API wants JSON, you get a 415 for a request that
looks entirely correct in your code.
multipart/form-data
The encoding for anything containing binary data. As shown above, the body is split into parts, each with its own headers, so file bytes travel unmodified. Use it for uploads; do not use it for a payload that is purely text and numbers, where it is far more verbose than JSON and harder to validate.
A common hybrid is worth knowing: send the file as one part and a JSON document as another, with that part
labelled Content-Type: application/json. You get structured, schema-validated metadata
alongside raw bytes in a single request.
application/json | x-www-form-urlencoded | multipart/form-data | |
|---|---|---|---|
| Nested structures | Yes | No | Per part only |
| Typed values | Yes | No — all strings | No — all strings |
| Binary data | Base64 only, ~33% larger | Percent-encoded, much larger | Yes, as-is |
| Per-field content type | No | No | Yes |
| Human-readable on the wire | Yes | Mostly | Verbose |
| Triggers a CORS preflight | Yes | No | No |
| Typical use | APIs | HTML forms, OAuth token endpoints | File uploads |
The preflight row is not a detail. Because application/json is not on the CORS safelist,
a browser must send an OPTIONS request before every JSON write — see
CORS explained for why the safelist contains exactly the three types an HTML
form could already produce.
Structured syntax suffixes: the +json family
A media type can name both a serialisation and the specific semantics layered on top of it, joined by a
plus sign. RFC 6839 calls this a structured syntax suffix, and it solves a real problem:
without it, a type such as application/problem+json would either have to be
application/json, losing the specific meaning, or an opaque type that generic tooling cannot
recognise as JSON at all.
With the suffix you get both. Any client can see the +json and parse the bytes with an
ordinary JSON parser; a client that recognises the full type also knows what the fields mean. The same
pattern gives +xml, +yaml, +cbor and +zip. Types you
will actually meet:
| Media type | Defined by | What it adds |
|---|---|---|
application/problem+json | RFC 9457 | A standard error document |
application/merge-patch+json | RFC 7386 | A JSON Merge Patch document |
application/json-patch+json | RFC 6902 | An array of patch operations |
application/vnd.api+json | JSON:API | That specification's document structure |
application/ld+json | W3C | Linked data with a context |
application/geo+json | RFC 7946 | Geographic features |
The patch types are the ones with immediate consequences: a PATCH body's media type names the
patch language, not the resource, and sending the wrong one is a straightforward
415. PUT vs PATCH covers the
formats themselves.
application/problem+json
Worth knowing on its own, because it answers a question every API designer faces: what shape should an error body be? RFC 9457 defines one, so you do not have to invent it:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://example.com/probs/insufficient-stock",
"title": "Insufficient stock",
"status": 422,
"detail": "Only 1 unit of TEA-004 remains; 2 were requested.",
"instance": "/v1/orders/ord_9f2c41"
}
type is a URI identifying the kind of problem and is the field machines should branch on;
title is a short human summary that stays constant for that type; status repeats
the HTTP status so the document survives being copied out of its response;
detail is specific to this occurrence and safe to log; instance identifies the
occurrence. Extension members are allowed, so a validation error can add a field-level array alongside the
standard ones. Two rules make it work in practice: the status code is still the primary signal and the
document must agree with it, and detail is for humans — never make clients parse prose.
Content negotiation, and the Accept header
Content-Type describes what is in this message. Accept describes what
the client would like in the response. They are not opposites and they frequently appear
together, meaning “I am sending JSON and I would like JSON back”.
Accept: application/json;q=1.0, application/xml;q=0.8, */*;q=0.1
The list is ordered by quality value, a weight from 0 to 1 defaulting to 1 when omitted.
Higher wins; q=0 means “not acceptable at all”. Specificity breaks ties, so
application/json outranks application/*, which outranks */*. The
server picks the best type it can produce and states its choice in the response's
Content-Type — negotiation is a request for a preference, never an instruction.
If the server can produce nothing on the list, it may answer
406 Not Acceptable. In practice many APIs serve their default
representation instead, which the specification permits and which is usually kinder; a hard
406 is most useful when the difference genuinely matters, such as an endpoint that can emit
CSV or JSON and must not guess.
Whenever a response varies by request headers, say so: Vary: Accept tells caches and CDNs
that this response is only valid for requests with a matching Accept. Omit it and a shared
cache will happily serve the XML representation to a client that asked for JSON — the same class of bug as
a missing Vary: Origin in CORS.
Sibling headers negotiate other dimensions and are frequently confused with this one:
Accept-Language for natural language, Accept-Encoding for compression (answered
by Content-Encoding), and Accept-Charset, which is deprecated because everything
is UTF-8 now. RFC 5789's Accept-Patch advertises which patch formats a resource understands
and belongs on a 415. The wider set is surveyed in the
HTTP headers guide.
Diagnosing Content-Type failures
| Symptom | Likely cause | Fix |
|---|---|---|
| 415 Unsupported Media Type | No Content-Type at all, or one the endpoint does not accept |
Set it explicitly; check whether your client form-encoded the body behind your back |
| 400 with a parse error | The type is accepted but the bytes do not match it | Validate the body against the declared format before blaming the endpoint |
| The body arrives empty or all fields are null | The server parsed it with the wrong parser — commonly JSON sent as form-encoded | Echo the request back from a service such as httpbin and read what it says it received |
| 406 Not Acceptable | Your Accept header excludes everything the endpoint can produce |
Widen it, or send Accept: */* to see what the default representation is |
| Accented characters arrive corrupted | Missing or wrong charset on a text/* body |
Send charset=utf-8 and confirm the bytes really are UTF-8 |
| A file upload fails or arrives truncated | A hand-written multipart header whose boundary does not match the body |
Let the client library build the whole multipart body and its header together |
An unexpected OPTIONS request in your logs |
A browser preflighting because application/json is not CORS-safelisted |
Answer the preflight; see CORS explained |
| A response renders as text instead of downloading | The server sent a generic type, or the browser overrode it | Send the accurate type, add Content-Disposition: attachment, and send X-Content-Type-Options: nosniff |
That last row is a security matter as much as a cosmetic one. Browsers historically guessed a body's type
from its bytes when the declared type looked wrong, which lets an uploaded file that you serve as
text/plain be re-interpreted as HTML and executed as script in your origin.
X-Content-Type-Options: nosniff disables the guessing — but only if the type you send is
accurate, so the header is a complement to getting Content-Type right, not a substitute.
When you cannot tell what your client is really sending, stop reading code and look at the wire. Send the
request from the API tester to an echo endpoint such as
https://httpbin.org/post, and the response shows the headers as received and the body as
parsed. That answers in ten seconds a question that framework documentation often does not answer at all —
and it is the same technique the REST API testing guide recommends
for isolating any header-shaped bug. Related size and range limits live nearby:
413 for a body too large and
411 for a missing Content-Length.
Frequently asked questions
Why do I get a 415 Unsupported Media Type?
The server read your Content-Type and does not accept that media type on that endpoint.
A missing header is the commonest trigger — a body with no declared type cannot be routed to a parser, and
most frameworks refuse rather than guess. Next commonest is sending
application/x-www-form-urlencoded where the endpoint wants
application/json.
Should I send charset=utf-8 with application/json?
No. The application/json registration defines no charset parameter, because
RFC 8259 already requires UTF-8 for JSON exchanged between systems. It is redundant, and a strict parser
may object. The parameter does matter for text/plain, text/html and especially
text/csv.
What does the plus sign in application/problem+json mean?
It is a structured syntax suffix from RFC 6839. The part after the plus names the serialisation, the part before it names the semantics layered on top — so that type is a JSON document any JSON parser can read, which also follows the problem details schema of RFC 9457.
What is the difference between Content-Type and Accept?
Content-Type describes the body present in this message, in either direction.
Accept is a request header stating a preference for what comes back, with optional quality
values. A request commonly carries both. A server that can satisfy nothing on the list may answer
406.
When should I use multipart/form-data instead of JSON?
When the payload contains binary data — usually a file. Multipart splits the body into boundary-separated parts, each with its own headers, so bytes travel as-is instead of being base64-inflated by a third. For text and numbers, JSON is simpler and far easier to validate.
Does Content-Type affect CORS?
Yes. Only application/x-www-form-urlencoded, multipart/form-data and
text/plain keep a cross-origin request simple — the types an HTML form could already send.
Anything else, application/json included, forces an OPTIONS preflight
first.