204 No Content
Success response defined by RFC 9110 §15.3.5.
Last updated July 27, 2026
Common causes at a glance
- A successful DELETE with nothing meaningful to return
- A PUT or PATCH that updated an existing resource in place
- A CORS preflight OPTIONS response
- A telemetry or beacon endpoint acknowledging receipt
Reproduce it
curl -i -X DELETE https://api.example.com/v1/widgets/8412What 204 No Content means
204 is the right answer when the caller already knows everything it needs
to know. A DELETE that worked, a PUT that replaced an existing
representation, a preference toggle, a beacon: none have anything useful to say, and
returning an empty JSON object instead costs bytes and forces the client to parse it.
The header block is still meaningful. An ETag on a 204 response to
PUT gives you the validator for the representation you just wrote, usable on
the next conditional update without a round trip.
What the spec says
RFC 9110 §15.3.5 states that a 204 response is
terminated by the end of the header section and cannot contain content or trailers, and
that response header fields refer to the target resource after the action was applied. It
is also heuristically cacheable (RFC 9111 §4.2.2), which surprises
people who use 204 for state-changing endpoints. The framing rule matters at the
transport layer: with no body, a Content-Length other than zero is a framing
error, not a hint. Compare 205, which also forbids content but
additionally asks the client to reset its form.
What actually causes it
Trouble with 204 is nearly always a client or middlebox that cannot
cope with a body that is not there. Older HTTP client wrappers call
response.json() unconditionally and blow up on an empty string; the fix is
in the caller. Proxies that inject a Content-Length or, worse, an error page
body onto a 204 create a framing violation that shows up as a stalled connection or
protocol error rather than a clean failure. CORS is the other big source: a preflight
OPTIONS answered with 204 is fine, but only if it carries the
Access-Control-Allow-* headers, and plenty of frameworks generate a bare 204
for OPTIONS with no CORS headers at all. The browser then reports an opaque
CORS failure while curl shows a happy 204, which is why the two disagree.
How to debug it
Confirm the response really is empty and framed correctly:
curl -s -o /dev/null -w 'status=%{http_code} bytes=%{size_download}\n' -X DELETE https://api.example.com/v1/widgets/8412
Anything other than zero bytes on a 204 means something in the path is appending a body.
For a CORS problem, replay the preflight as the browser would, with both
Origin and Access-Control-Request-Method, and read the response
headers rather than the status. A 204 with no
access-control-allow-origin is the failure, even though the status looks
successful.
curl -i -X OPTIONS -H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: DELETE' https://api.example.com/v1/widgets/8412
The curl command builder can assemble that preflight for you.
Working examples
curl
# A 204 must download zero bytes. Anything else is a broken intermediary.
curl -s -o /dev/null -w 'status=%{http_code} bytes=%{size_download}\n' \
-X DELETE https://api.example.com/v1/widgets/8412
# Replay a CORS preflight and check the headers, not the status:
curl -i -X OPTIONS \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: DELETE' \
-H 'Access-Control-Request-Headers: authorization' \
https://api.example.com/v1/widgets/8412
Python (requests)
import requests
resp = requests.delete("https://api.example.com/v1/widgets/8412", timeout=10)
print(resp.status_code, len(resp.content)) # 204 0
# resp.json() raises on an empty body. Guard on the status code first.
data = resp.json() if resp.status_code != 204 and resp.content else None
print(data)
# On a PUT, the ETag from a 204 is the validator for your next conditional write.
put = requests.put(
"https://api.example.com/v1/widgets/8412",
json={"size": 4},
headers={"If-Match": 'W/"a1b2c3"'},
timeout=10,
)
print(put.status_code, put.headers.get("ETag"))
Node (fetch)
const res = await fetch("https://api.example.com/v1/widgets/8412", {
method: "DELETE",
});
console.log(res.status, res.ok); // 204 true
// res.json() rejects on an empty body; branch on the status before parsing.
const payload = res.status === 204 ? null : await res.json();
console.log(payload);
// Preflights are issued by the browser, but you can reproduce one directly.
const pre = await fetch("https://api.example.com/v1/widgets/8412", {
method: "OPTIONS",
headers: {
Origin: "https://app.example.com",
"Access-Control-Request-Method": "DELETE",
},
});
console.log(pre.status, pre.headers.get("access-control-allow-origin"));Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.