Skip to content
Closed
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
13 changes: 12 additions & 1 deletion src/h2/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@
from .config import H2Configuration


def _is_bodyless_response(headers: Iterable[Header]) -> bool:
"""Whether a response status is defined by RFC 9113 to have no content."""
for name, value in headers:
if name in (b":status", ":status"):
return value in (b"204", b"304", "204", "304")
return False


class StreamState(IntEnum):
IDLE = 0
RESERVED_REMOTE = 1
Expand Down Expand Up @@ -1069,6 +1077,9 @@ def receive_headers(self,
"""
Receive a set of headers (or trailers).
"""
# Concretize once: the same block is inspected for informational status,
# content-length, and header validation below.
headers = list(headers)
if is_informational_response(headers):
if end_stream:
msg = "Cannot set END_STREAM on informational responses"
Expand Down Expand Up @@ -1360,7 +1371,7 @@ def _initialize_content_length(self, headers: Iterable[Header]) -> None:
_expected_content_length field from it. It's not an error for no
Content-Length header to be present.
"""
if self.request_method == b"HEAD":
if self.request_method == b"HEAD" or _is_bodyless_response(headers):
self._expected_content_length = 0
return

Expand Down
81 changes: 81 additions & 0 deletions tests/test_bodyless_responses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""RFC 9113 bodyless responses may carry a non-zero content-length."""

from __future__ import annotations

import pytest

import h2.connection
import h2.events
import h2.exceptions

REQUEST_HEADERS = [
(b":authority", b"example.com"),
(b":path", b"/"),
(b":scheme", b"https"),
(b":method", b"POST"),
]


def _open_stream(frame_factory) -> h2.connection.H2Connection:
connection = h2.connection.H2Connection()
connection.initiate_connection()
connection.send_headers(1, REQUEST_HEADERS, end_stream=True)
return connection


@pytest.mark.parametrize("status", [b"204", b"304"])
def test_bodyless_response_accepts_content_length_with_empty_data(
frame_factory, status: bytes
) -> None:
connection = _open_stream(frame_factory)
headers = frame_factory.build_headers_frame(
[(b":status", status), (b"content-length", b"1234")]
)
events = connection.receive_data(headers.serialize())
assert isinstance(events[0], h2.events.ResponseReceived)

data = frame_factory.build_data_frame(b"", flags=["END_STREAM"])
events = connection.receive_data(data.serialize())

assert isinstance(events[0], h2.events.DataReceived)
assert isinstance(events[1], h2.events.StreamEnded)


@pytest.mark.parametrize("status", [b"204", b"304"])
def test_bodyless_response_accepts_content_length_when_headers_end_stream(
frame_factory, status: bytes
) -> None:
connection = _open_stream(frame_factory)
headers = frame_factory.build_headers_frame(
[(b":status", status), (b"content-length", b"1234")],
flags=["END_STREAM"],
)

events = connection.receive_data(headers.serialize())

assert isinstance(events[0], h2.events.ResponseReceived)
assert isinstance(events[1], h2.events.StreamEnded)


def test_200_with_content_length_and_empty_data_is_still_rejected(frame_factory) -> None:
connection = _open_stream(frame_factory)
headers = frame_factory.build_headers_frame(
[(b":status", b"200"), (b"content-length", b"1234")]
)
connection.receive_data(headers.serialize())

data = frame_factory.build_data_frame(b"", flags=["END_STREAM"])
with pytest.raises(h2.exceptions.InvalidBodyLengthError):
connection.receive_data(data.serialize())


def test_bodyless_response_with_actual_data_is_rejected(frame_factory) -> None:
connection = _open_stream(frame_factory)
headers = frame_factory.build_headers_frame(
[(b":status", b"304"), (b"content-length", b"1234")]
)
connection.receive_data(headers.serialize())

data = frame_factory.build_data_frame(b"\x01", flags=["END_STREAM"])
with pytest.raises(h2.exceptions.InvalidBodyLengthError):
connection.receive_data(data.serialize())