Bodies, headers and status codes

Which headers you control, how to read the response body, and how to branch on status codes without duplicating error handling everywhere.

Request and response headers

HeaderSet it when
Content-Typeyou send a body: application/json, application/x-www-form-urlencoded, multipart/form-data
Acceptyou want to declare what you can parse
Authorizationthe API uses a bearer token
If-None-Matchyou send back a stored ETag and accept a 304
X-CSRF-Tokena same-site cookie session needs a token for writes
const res = await fetch('/api/me', {
  headers: {
    'Accept': 'application/json',
    'Authorization': 'Bearer ' + token,
    'X-CSRF-Token': getCsrfToken()
  }
});

res.headers.get('content-type');     // 'application/json; charset=utf-8'
res.headers.get('cache-control');
res.headers.get('link');             // pagination, when the API sends it

Some headers can never be set from script: Host, Content-Length, Cookie and Origin belong to the browser. On the way back, only a small list of response headers is readable unless the server names the others in Access-Control-Expose-Headers.

Reading the response body

const res = await fetch('/report.csv');

const text = await res.text();          // any text payload
const blob = await res.blob();          // binary; feed it to a download link
const buf  = await res.arrayBuffer();   // bytes for decoding yourself
const data = await res.json();          // JSON.parse on the body text

// stream a large body instead of buffering all of it
const reader = res.body.getReader();
const { value, done } = await reader.read();
  • res.json() parses the body; it does not check that the endpoint really is a JSON one.
  • res.status is the code, res.statusText the reason phrase, and res.url the final URL after any redirects.
  • res.redirected tells you whether a redirect happened; the original request URL is not available on the response.
  • Streaming with res.body is how progressive rendering and row-by-row CSV processing are built.

Status codes worth branching on

CodeMeaningWhat the client should do
200OKread the body
201Createdread Location, refresh the list
204No Contentdo not try to parse a body
400Bad Requestshow validation errors from the body
401Unauthorizedrefresh the token or prompt for login
403Forbiddenshow a permission message
404Not Foundrender an empty state
409Conflictre-fetch and retry once
429Too Many Requestswait for Retry-After, then back off
500, 502, 503Server errorretry with backoff and a cap
async function request(url, init) {
  const res = await fetch(url, init);
  if (res.status === 204) return null;
  if (res.status === 401) { await refresh(); return request(url, init); }
  if (res.status === 429) {
    const wait = Number(res.headers.get('retry-after') || 5);
    throw new Error('rate limited, retry in ' + wait + 's');
  }
  if (!res.ok) throw new Error('HTTP ' + res.status);
  return res.json();
}
⚠️
Retrying a non-idempotent request can duplicate data. Only replay GET, PUT, DELETE and HEAD automatically, or make the write safe to repeat with a client-generated idempotency key.

FAQ

Should I always send Content-Type?
Only when you send a body. Declaring application/json while actually posting form data makes the server parse nothing, and sending the header with an empty body confuses some frameworks.
Why is res.status 0?
The request never completed: a network failure, a blocked mixed-content request, or a fetch that was aborted. Treat 0 as 'no response' rather than as a server error.

Requests with fetch HTTP status codes

Last refreshed 2026-09-18.