Content-Type

One header, three components, and an outsized share of the requests that fail for no visible reason. Content-Type is how a message declares what its body is — not a hint, not metadata, but the instruction that decides which parser the bytes are handed to. This guide covers the grammar in full, the parameters that matter, the three encodings a request body actually uses in practice, structured syntax suffixes, and how negotiation works from the other side. Every example is reproducible from the API tester.

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:

TreeFormMeaning
Standardsapplication/jsonRegistered with IANA through a standards process
Vendorapplication/vnd.api+jsonDefined by a specific product or organisation
Personalapplication/prs.examplePersonal or experimental, unregistered
Unregisteredapplication/x.customPrivate use, never to be registered
Legacy x-application/x-www-form-urlencodedAn 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 no charset parameter 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-8 is nonetheless extremely widespread and almost always tolerated.
  • text/*: send it. For text/plain, text/html and especially text/csv, the encoding is genuinely ambiguous, and getting it wrong is how a name with an accent in it becomes three replacement characters. Say charset=utf-8 explicitly 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&notes=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/jsonx-www-form-urlencodedmultipart/form-data
Nested structuresYesNoPer part only
Typed valuesYesNo — all stringsNo — all strings
Binary dataBase64 only, ~33% largerPercent-encoded, much largerYes, as-is
Per-field content typeNoNoYes
Human-readable on the wireYesMostlyVerbose
Triggers a CORS preflightYesNoNo
Typical useAPIsHTML forms, OAuth token endpointsFile 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 typeDefined byWhat it adds
application/problem+jsonRFC 9457A standard error document
application/merge-patch+jsonRFC 7386A JSON Merge Patch document
application/json-patch+jsonRFC 6902An array of patch operations
application/vnd.api+jsonJSON:APIThat specification's document structure
application/ld+jsonW3CLinked data with a context
application/geo+jsonRFC 7946Geographic 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

SymptomLikely causeFix
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.