FastAPI: Resolving Status Code 204 No Content Response Body Violation
This error occurs when Python tries to return a response body with HTTP status 204 No Content. Status 204 prohibits any message body per RFC 9110. Return an empty Response(status_code=status.HTTP_204_NO_CONTENT) instead.
Root Cause Analysis
This error occurs when Python web applications running FastAPI or Starlette attempt to return a response payload or JSON body alongside HTTP status code 204 (No Content).
Root Cause 1: RFC 9110 / RFC 7230 HTTP Specification Violation
The official HTTP specification (RFC 9110 Section 15.3.5) strictly specifies that a 204 No Content response must never contain a message body. Any Content-Length or Transfer-Encoding header with non-zero bytes or actual chunk data sent on a 204 response is an explicit protocol violation. Starlette and ASGI web servers enforce this rule strictly to maintain protocol compliance.
Root Cause 2: FastAPI Route Default Return Behavior
When a FastAPI route decorator is defined with @app.delete('/items/{id}', status_code=status.HTTP_204_NO_CONTENT) and the python function returns a dictionary (e.g. return {'message': 'Deleted successfully'}), FastAPI attempts to serialize the dictionary into JSON and send it in the response body. This conflict between the 204 status code and the JSON payload triggers an AssertionError or leads to corrupted client connections.
Root Cause 3: Reverse Proxy and Gateway Rejections
Production reverse proxies such as Nginx, Traefik, AWS ALB, and Cloudflare will actively drop or terminate connections that transmit payload bytes on 204 responses. This causes unpredictable ERR_HTTP2_PROTOCOL_ERROR or 502 Bad Gateway errors on frontend clients.
Root Cause 4: Confusing 204 No Content with 200 OK or 202 Accepted
Developers often choose 204 because a deletion or update succeeded, but still wish to return an acknowledgment message, updated metadata, or a confirmation ID. If data must be returned to the client, HTTP 200 OK or HTTP 202 Accepted should be used instead.
Reproduction Code (MCVE)
def build_http_response(status_code: int, payload: dict | None):
# Enforcing strict HTTP RFC 9110 compliance: 204 responses must not have a body
if status_code == 204 and payload is not None and len(payload) > 0:
raise AssertionError(
"HTTP Protocol Error: Status code 204 (No Content) must not include a response body. "
f"Received payload: {payload}. Return an empty Response or use status code 200 OK."
)
return {"status_code": status_code, "body": payload}
# Simulating an endpoint configured with 204 returning a JSON message
build_http_response(204, {"detail": "Item successfully deleted from database"})
Solution 1: Return an Empty Response Object with Status 204
Return a raw Response(status_code=status.HTTP_204_NO_CONTENT) with no content body, or return None from an async route function configured with status code 204.
from fastapi import FastAPI, Response, status
app = FastAPI()
# Database simulation
database = {"item_1": "Active Record", "item_2": "Archived Record"}
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: str):
if item_id in database:
del database[item_id]
# Return empty Response with 204 status code — no body attached
return Response(status_code=status.HTTP_204_NO_CONTENT)
# Standalone execution test
res = Response(status_code=status.HTTP_204_NO_CONTENT)
print("Response Status Code:", res.status_code)
print("Response Body Bytes:", res.body)
assert res.status_code == 204
assert res.body == b""
Solution 2: Change Status Code to 200 OK When Returning JSON
If your frontend requires a confirmation message or deleted entity metadata, change the response status code to 200 OK.
from fastapi import FastAPI, status
from pydantic import BaseModel
app = FastAPI()
class DeleteResponse(BaseModel):
success: bool
deleted_id: str
message: str
@app.delete("/items/{item_id}", response_model=DeleteResponse, status_code=status.HTTP_200_OK)
async def delete_item_with_feedback(item_id: str):
return DeleteResponse(
success=True,
deleted_id=item_id,
message="Resource was deleted successfully from the database"
)
# Standalone execution test
response_data = DeleteResponse(success=True, deleted_id="item_42", message="Deleted")
print(response_data.model_dump())
A common trap in unit testing with httpx.AsyncClient or starlette.testclient.TestClient is attempting to call response.json() on a 204 response. Because a 204 response contains an empty byte string (b''), calling response.json() will raise a json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0). Always assert response.status_code == 204 and check response.text == ''.
Another edge case occurs with HTTP 304 Not Modified and 205 Reset Content. Similar to 204, status 304 and 205 have strict restrictions regarding message bodies.
Contrast status 204 with status 202 Accepted: 204 signifies that the request has completed fully and there is nothing to return; 202 signifies that the request has been queued or accepted for asynchronous processing and may return a task ID or polling URL.