Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions gunicorn/http/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,10 @@ def process_headers(self, headers):
value = value.strip(" \t")
lname = name.lower()
if lname == "content-length":
# RFC 9112 6.3: 1xx, 204, and 304 responses must not
# contain a message body, so Content-Length is stripped.
if self.status_code in (204, 304):
Copy link
Contributor

@pajod pajod Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excessive. Content-Length for 304 responses is perfectly reasonable.. it expresses the (potentially non-zero) size as usual, it is merely not to be used for determining message framing.

continue
self.response_length = int(value)
elif util.is_hoppish(name):
if lname == "connection":
Expand Down Expand Up @@ -395,6 +399,12 @@ def write(self, arg):
self.send_headers()
if not isinstance(arg, bytes):
raise TypeError('%r is not a byte' % arg)

# RFC 9112 6.3: 204 and 304 responses must not contain
# a message body. Silently discard any body data.
if self.status_code in (204, 304):
return

arglen = len(arg)
tosend = arglen
if self.response_length is not None:
Expand Down
63 changes: 63 additions & 0 deletions tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,69 @@ def test_http_invalid_response_header():
response.start_response("200 OK", [('foo\r\n', 'essai')])


def test_204_response_strips_content_length_and_body():
"""RFC 9112: 204 responses must not contain body or Content-Length."""
mocked_socket = mock.MagicMock()
mocked_request = mock.MagicMock()
mocked_request.version = (1, 1)
mocked_request.method = 'GET'

response = Response(mocked_request, mocked_socket, None)
response.start_response("204 No Content", [
('Content-Length', '5'),
('X-Custom', 'keep'),
])

# Content-Length should be stripped
header_names = [h[0].lower() for h in response.headers]
assert 'content-length' not in header_names
assert 'x-custom' in header_names
assert response.response_length is None

# Body writes should be silently discarded
response.write(b"hello")
assert response.sent == 0


def test_304_response_strips_content_length_and_body():
"""RFC 9112: 304 responses must not contain body or Content-Length."""
mocked_socket = mock.MagicMock()
mocked_request = mock.MagicMock()
mocked_request.version = (1, 1)
mocked_request.method = 'GET'

response = Response(mocked_request, mocked_socket, None)
response.start_response("304 Not Modified", [
('Content-Length', '100'),
('ETag', '"abc"'),
])

header_names = [h[0].lower() for h in response.headers]
assert 'content-length' not in header_names
assert 'etag' in header_names
assert response.response_length is None

response.write(b"x" * 100)
assert response.sent == 0


def test_200_response_keeps_content_length_and_body():
"""Normal responses should keep Content-Length and body."""
mocked_socket = mock.MagicMock()
mocked_request = mock.MagicMock()
mocked_request.version = (1, 1)
mocked_request.method = 'GET'

response = Response(mocked_request, mocked_socket, None)
response.start_response("200 OK", [
('Content-Length', '5'),
])

assert response.response_length == 5
response.write(b"hello")
assert response.sent == 5


def test_unreader_read_when_size_is_none():
unreader = Unreader()
unreader.chunk = mock.MagicMock(side_effect=[b'qwerty', b'123456', b''])
Expand Down
Loading