Bodies and content types

HTTP & the web · Essentials · 10 min

Mismatched content types are behind countless '400 Bad Request' and 'body is empty' bugs.

A body is just bytes; Content-Type tells the receiver how to interpret them, using a media type (MIME type) plus optional parameters.

  • application/json: the default for APIs. UTF-8 is required by the JSON spec, so no charset parameter is needed.
  • text/html; charset=utf-8, text/plain, text/css, text/javascript.
  • application/x-www-form-urlencoded: HTML form default: name=Ada&lang=en.
  • multipart/form-data; boundary=...: forms with file uploads. Each field is a part separated by a boundary string.
  • application/octet-stream: arbitrary binary data. Images, audio and video have their own types: image/png, image/avif, video/mp4.

Servers pick a parser based on the request's Content-Type. Send JSON without the header and many frameworks see an empty or text body; send the wrong one and you get 415 Unsupported Media Type.

The size comes from Content-Length, or, when the size isn't known in advance, from streaming: chunked transfer encoding in HTTP/1.1, or frames in HTTP/2 and 3.

Content-Disposition: attachment; filename="report.pdf" asks a browser to download rather than display a response.

Browsers may sniff content when the type is missing or vague; send X-Content-Type-Options: nosniff so a file uploaded as an image can't be executed as script. Always set an accurate type on responses, including charset=utf-8 for text.

Example

// JSON body: set the header AND serialise
await fetch("https://httpbin.org/post", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Ada", langs: ["en", "fr"] }),
});

// URL-encoded form body (fetch sets the Content-Type for URLSearchParams)
await fetch("https://httpbin.org/post", {
  method: "POST",
  body: new URLSearchParams({ name: "Ada Lovelace", lang: "en" }),  // name=Ada+Lovelace&lang=en
});

// Multipart with a file (fetch sets multipart/form-data with a boundary)
const form = new FormData();
form.append("title", "Avatar");
form.append("file", new Blob(["fake png bytes"], { type: "image/png" }), "a.png");
const res = await fetch("https://httpbin.org/post", { method: "POST", body: form });
console.log(res.headers.get("content-type"));   // application/json

Remember

  • Content-Type tells the receiver how to parse the body
  • JSON APIs use application/json; forms use urlencoded or multipart
  • File uploads need multipart/form-data
  • Send nosniff and accurate types on responses

Watch out for

  • Setting Content-Type: multipart/form-data by hand with fetch, which drops the boundary parameter; let fetch set it.

Check yourself

  1. You POST JSON with fetch but forget the Content-Type header. What commonly happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Type