Konrad Kowalski (rootsher)Principal Platform & Reliability Architect001011010111101000000000001111110000001010000000

Headers and Body: Metadata vs Representation

date
category
Frontend
also in
Backend
reading
1 min / 275 words

A typical request:

js
fetch("/api/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    name: "Alice"
  })
});

It is easy to treat Content-Type as boilerplate.

It is not.

A body is bytes, not JSON

For HTTP the body is content transferred as a sequence of bytes.

It is the header that tells the recipient how to interpret it:

http
Content-Type: application/json

Other possibilities:

text
text/plain
text/html
application/octet-stream
multipart/form-data
image/webp

HTTP does not require JSON.

JSON is a choice of the application layer.

Content-Type describes what we are sending

http
Content-Type: application/json

means:

the content of this message is JSON.

Whereas:

http
Accept: application/json

means:

the client prefers JSON as the representation of the response.

Those are two different directions of negotiation.

text
Content-Type -> what I am sending
Accept       -> what I want back

The browser can build the body for us

With FormData:

js
const form = new FormData();

form.append("name", "Alice");
form.append("avatar", file);

fetch("/profile", {
  method: "POST",
  body: form
});

We should not set this by hand:

http
Content-Type: multipart/form-data

The browser has to add the correct boundary.

For example:

text
multipart/form-data; boundary=----browser-generated

This is a case where manual control over HTTP makes the request worse.

Content-Encoding is a different layer

A response can carry:

http
Content-Type: application/json
Content-Encoding: gzip

Semantically the representation is JSON.

On the wire the content was additionally encoded with compression.

The browser usually decompresses it before exposing it through the Fetch API.

The frontend gets:

js
await response.json();

and not compressed gzip bytes to unpack by hand.

Content-Length is not always needed

If the size of the content is known, this can appear:

http
Content-Length: 842

But a streaming response may be produced gradually.

The full size then does not have to be known up front.

Which matters for the frontend:

text
response headers available
!=
entire body downloaded

That is why Response.body can be a ReadableStream.

Headers steer the browser's behavior

Headers are not just metadata for the backend.

They can affect:

text
cache
cookies
content interpretation
redirects
CORS
download behavior
security policy

But JavaScript does not fully control them.

The browser is an active participant in HTTP and builds part of the request itself.

That is the subject of the next article.