¿Odias los anuncios? Ir Sin publicidad Hoy

Content-Type and MIME Types — What That Header Actually Controls (and Why charset Breaks JSON Parsers)

Publicado el

You probably set Content-Type by habit. Here's what actually happens when you get it wrong: silent body drops, broken JSON parsers, upload failures. A practical breakdown of MIME types, charset, boundaries, and how to stop guessing.

Content-Type and MIME Types — What That Header Actually Controls (and Why charset Breaks JSON Parsers) 1
ANUNCIO · ¿ELIMINAR?

You set Content-Type: application/json and your server eats the request body. You send a file upload and the backend yells about a missing boundary. You add charset=utf-8 to your JSON endpoint and your parser chokes. These aren’t random bugs — they’re predictable consequences of a header that most of us half-understand.

El Content-Type header tells the receiver two things: what kind of data this is (the MIME type) and how to decode it (via parameters like charset o boundary). Get either part wrong and the receiver either rejects the body outright or silently misparses it. Neither is fun to debug at 11pm.

The structure of a Content-Type header

The full syntax is:

Content-Type: type/subtype; parameter=value

The type and subtype form the MIME type (application/json, text/html, image/png). The parameter is optional metadata that modifies how the content should be handled. The two parameters you’ll actually encounter are charset y boundary, and they behave very differently depending on the MIME type.

text/html vs application/json — they’re not interchangeable

Both of these carry text-based content. The difference is who reads them and how.

text/html tells the browser: render this as a document. It triggers full HTML parsing, layout, and rendering. Serve application/json where the browser expects text/html and you’ll see raw JSON in the viewport instead of a rendered page.

application/json tells the receiver: parse this as a JSON object. No rendering, no special encoding — just deserialize the bytes. The application/ prefix means the data is meant for an application to process, not a human to read directly.

Where this matters most: CORS preflight requests. Browsers treat application/json as a non-simple content type and fire an OPTIONS preflight before the actual request. text/plain doesn’t trigger preflight. This is why some legacy APIs accept JSON but advertise text/plain — it sidesteps preflight at the cost of correctness. Don’t do this in new code.

Why charset breaks JSON parsers

This one comes up constantly. You see an API with:

Content-Type: application/json; charset=utf-8

Looks reasonable. UTF-8 JSON is fine, right? The problem is that RFC 7159 (and its successor RFC 8259) explicitly state that JSON MUST be encoded in UTF-8 and that the charset parameter has no effect on JSON processing. A conformant JSON parser reads the bytes and decodes them as UTF-8 regardless of what charset says.

The real breakage happens when strict parsers enforce RFC 9110 and reject content types they don’t recognize as valid. Some frameworks tokenize the full content type string and fail if they get application/json; charset=utf-8 instead of a bare application/json. Spring’s default MappingJackson2HttpMessageConverter accepts both, but stricter validators in some Go and Rust HTTP libraries will match only the exact media type — add charset=utf-8 and your request returns a 415 Unsupported Media Type.

The fix: send Content-Type: application/json for JSON endpoints. No charset. If you need to be explicit about encoding, document it — JSON is always UTF-8 by spec.

multipart/form-data and the boundary you can’t skip

File uploads break in a specific way that’s immediately recognizable once you know what to look for. The request arrives, the server parses it, and the file array is empty. No error. Just nothing.

The reason: multipart/form-data is different from every other content type because the body contains multiple parts, each with its own headers, separated by a delimiter. That delimiter is the boundary parameter:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

Without the boundary, the server has no way to split the body into parts. It can’t find where one field ends and the next begins. The entire body is treated as unparseable noise.

The most common way to trigger this bug: manually setting Content-Type: multipart/form-data in your fetch/axios request. As soon as you set it manually, the browser (or HTTP client) no longer generates the boundary automatically. The header exists, the body is multipart, but the boundary is missing from the header — and the server silently drops all fields.

La solución: For file uploads using FormData, don’t set Content-Type at all. Let the browser or HTTP client set it. They’ll generate a correct boundary and wire it into the header automatically.

// WRONG — kills the auto-generated boundary
fetch('/upload', {
  method: 'POST',
  headers: { 'Content-Type': 'multipart/form-data' }, // ← don't do this
  body: formData,
});

// CORRECT — let the browser handle Content-Type
fetch('/upload', {
  method: 'POST',
  body: formData, // browser sets Content-Type + boundary automatically
});

application/x-www-form-urlencoded — the other form encoding

application/x-www-form-urlencoded is what HTML forms send by default when there’s no file input. The body looks like a query string:

name=Alice&age=30&city=New+York

Keys and values are percent-encoded (spaces become + o %20). No parts, no boundaries — just a flat key-value string.

Two places where developers mix this up with JSON:

  • Sending JSON as a form body — setting Content-Type: application/x-www-form-urlencoded but sending a JSON string in the body. The server’s form parser reads it as a malformed key-value string and silently produces garbage or an empty object.
  • Sending form data to a JSON endpoint — the opposite. The server expects application/json, gets a form-encoded body, and returns 400 or silently ignores the body entirely.

If you’re building a curl command to hit a form endpoint, the Generador de Comandos cURL can generate the right flags and content type headers automatically — useful for verifying what your frontend is actually sending.

Common MIME types — what they signal and what breaks when you get them wrong

Tipo MIMECorrect use caseWhat breaks when misused
application/jsonREST API request/response bodiesAgregar charset=utf-8 causes 415 errors on strict parsers; sending form data under this type produces empty request bodies
text/htmlBrowser-rendered HTML documentsServing JSON as text/html renders raw text; serving HTML as application/octet-stream triggers a download instead of rendering
multipart/form-dataFile uploads, forms with binary dataFalta boundary parameter makes the body unparseable — server sees empty fields regardless of what the body contains
application/x-www-form-urlencodedHTML form submissions (text fields, no files)Sending JSON in this content type produces malformed key-value pairs; binary data gets corrupted by percent-encoding
application/octet-streamUnknown binary data, generic file downloadsForces download in browser; HTTP clients that parse response bodies may fail to deserialize a meaningful response
text/plainPlain text, simple log outputUsing as a workaround to avoid CORS preflight is a footgun — proxies and CDNs may interpret and transform content differently
application/xml / text/xmlXML data exchangetext/xml defaults to US-ASCII charset (per RFC); application/xml defaults to UTF-8 — mixing them causes encoding errors on non-ASCII characters
image/jpeg, image/png, image/webpRaster image responsesWrong subtype causes some browsers to refuse rendering; serving image/jpeg for a PNG can corrupt image parsers on strict clients
application/pdfPDF documentsServing as application/octet-stream always triggers download; serving as text/plain renders PDF bytecode in the browser as garbage
multipart/mixedEmail attachments, batch API responsesBoundary handling same as multipart/form-data — missing boundary parameter = no parsing. Less common in HTTP but used in Gmail API and some batch endpoints

How proxies and CDNs use Content-Type

Content-Type isn’t just between your client and server. Reverse proxies and CDNs read it too, and they make caching and transformation decisions based on it.

Cloudflare, for example, will only apply Minify (JS/CSS/HTML compression) to responses that carry the correct content types. Serve JavaScript as application/octet-stream and it won’t be minified. More importantly, some CDNs vary their cache behavior: HTML is cached with different TTLs than JSON, and application/json responses may bypass certain cache layers entirely depending on configuration.

Nginx’s gzip_types directive uses MIME types to decide what to compress. If your API responses carry application/json and you haven’t listed it in gzip_types, they’re not getting compressed — even if gzip is enabled globally. The default gzip_types in most distro configs only includes text/html.

Sniffing: when browsers ignore Content-Type

If you serve a response without a Content-Type header (or with an obviously wrong one), browsers don’t just give up — they sniff the content. They look at the first few bytes and guess: is this an HTML document? A script? An image?

This is called MIME sniffing and it’s a security hole. A file upload endpoint that doesn’t validate file type can be exploited by serving an HTML file with an image MIME type — the browser might sniff it as HTML, execute it, and you have an XSS vector. The X-Content-Type-Options: nosniff response header tells browsers to trust the declared type and refuse to sniff. Set it on all responses you control.

You can inspect what headers your server is actually sending with the Analizador de Encabezados HTTP — paste a response or URL and see exactly what Content-Type y X-Content-Type-Options values are being returned.

Debugging Content-Type mismatches in practice

When something breaks and you suspect Content-Type, the fastest way to confirm is to check both sides of the exchange: what your client is sending and what your server is seeing.

On the client side: browser DevTools → Network tab → select the request → Headers panel. Look at the Request Headers section, not Response Headers. You want to see the actual outgoing Content-Type, not what you think you set in code.

On the server side (Node/Express): req.headers['content-type']. Flask: request.content_type. Django: request.content_type. Log it on the first line of your handler before anything else touches the body.

When you need to test quickly with curl, the Generador de Comandos cURL saves time — you can set the method, headers, and body format and get the exact command to paste into your terminal.

Content-Type is one of those headers that feels like ceremony until it breaks something. The short version: bare application/json for JSON, no charset; never manually set multipart/form-data when using FormData; match your content type to what the receiver actually expects. When in doubt, check the actual outgoing headers in DevTools — the gap between what you think you’re sending and what’s actually on the wire is usually where the bug lives.

¿Quieres eliminar publicidad? Adiós publicidad hoy

Instalar extensiones

Agregue herramientas IO a su navegador favorito para obtener acceso instantáneo y búsquedas más rápidas

añadir Extensión de Chrome añadir Extensión de borde añadir Extensión de Firefox añadir Extensión de Opera

¡El marcador ha llegado!

Marcador es una forma divertida de llevar un registro de tus juegos, todos los datos se almacenan en tu navegador. ¡Próximamente habrá más funciones!

ANUNCIO · ¿ELIMINAR?
ANUNCIO · ¿ELIMINAR?
ANUNCIO · ¿ELIMINAR?

Noticias Aspectos técnicos clave

Involucrarse

Ayúdanos a seguir brindando valiosas herramientas gratuitas

Invítame a un café
ANUNCIO · ¿ELIMINAR?