# -*- coding: utf-8 - # # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. """Tests for HTTP/2 request and body classes.""" import pytest from gunicorn.http2.request import HTTP2Request, HTTP2Body from gunicorn.http2.stream import HTTP2Stream class MockConnection: """Mock HTTP/2 connection for testing.""" def __init__(self, initial_window_size=65535): self.initial_window_size = initial_window_size class MockConfig: """Mock gunicorn configuration.""" def __init__(self): pass class TestHTTP2Body: """Test HTTP2Body class.""" def test_init_with_data(self): body = HTTP2Body(b"Hello, World!") assert len(body) == 13 def test_init_empty(self): body = HTTP2Body(b"") assert len(body) == 0 def test_read_all(self): body = HTTP2Body(b"Test data") assert body.read() == b"Test data" assert body.read() == b"" # Already consumed def test_read_with_size(self): body = HTTP2Body(b"Hello, World!") assert body.read(5) == b"Hello" assert body.read(2) == b", " assert body.read(100) == b"World!" assert body.read(1) == b"" def test_read_none_size(self): body = HTTP2Body(b"Test") assert body.read(None) == b"Test" def test_readline_basic(self): body = HTTP2Body(b"Line1\nLine2\nLine3") assert body.readline() == b"Line1\n" assert body.readline() == b"Line2\n" assert body.readline() == b"Line3" def test_readline_with_size(self): body = HTTP2Body(b"Hello\nWorld") assert body.readline(3) == b"Hel" assert body.readline(10) == b"lo\n" def test_readline_no_newline(self): body = HTTP2Body(b"No newline here") assert body.readline() == b"No newline here" def test_readline_empty(self): body = HTTP2Body(b"") assert body.readline() == b"" def test_readline_crlf(self): body = HTTP2Body(b"Line1\r\nLine2") # BytesIO readline includes \r\n assert body.readline() == b"Line1\r\n" def test_readlines_basic(self): body = HTTP2Body(b"Line1\nLine2\nLine3") lines = body.readlines() assert lines == [b"Line1\n", b"Line2\n", b"Line3"] def test_readlines_with_hint(self): body = HTTP2Body(b"Line1\nLine2\nLine3\nLine4") # Hint affects how many lines are returned lines = body.readlines(hint=5) assert len(lines) >= 1 def test_readlines_empty(self): body = HTTP2Body(b"") assert body.readlines() == [] def test_iter(self): body = HTTP2Body(b"Line1\nLine2\nLine3") lines = list(body) assert lines == [b"Line1\n", b"Line2\n", b"Line3"] def test_len(self): body = HTTP2Body(b"12345") assert len(body) == 5 def test_close(self): body = HTTP2Body(b"test") body.close() # Should not raise with pytest.raises(ValueError): body.read() class TestHTTP2BodyReadStrategies: """Test different reading strategies matching HTTP/1.x patterns.""" def test_read_all_at_once(self): data = b"A" * 1000 body = HTTP2Body(data) result = body.read() assert result == data def test_read_chunked(self): data = b"A" * 100 body = HTTP2Body(data) chunks = [] while True: chunk = body.read(10) if not chunk: break chunks.append(chunk) assert b"".join(chunks) == data assert len(chunks) == 10 def test_read_byte_by_byte(self): data = b"Hello" body = HTTP2Body(data) result = [] for _ in range(len(data)): result.append(body.read(1)) assert b"".join(result) == data def test_readline_all_lines(self): data = b"Line1\nLine2\nLine3\n" body = HTTP2Body(data) lines = [] while True: line = body.readline() if not line: break lines.append(line) assert lines == [b"Line1\n", b"Line2\n", b"Line3\n"] class TestHTTP2Request: """Test HTTP2Request class.""" def _make_stream(self, headers, body=b""): """Helper to create a stream with headers and body.""" conn = MockConnection() stream = HTTP2Stream(stream_id=1, connection=conn) stream.receive_headers(headers, end_stream=(len(body) == 0)) if body: stream.request_body.write(body) stream.request_complete = True return stream def test_basic_get_request(self): stream = self._make_stream([ (':method', 'GET'), (':path', '/test'), (':scheme', 'https'), (':authority', 'example.com'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.method == 'GET' assert req.uri == '/test' assert req.path == '/test' assert req.scheme == 'https' assert req.version == (2, 0) def test_post_request_with_body(self): stream = self._make_stream( [ (':method', 'POST'), (':path', '/submit'), (':scheme', 'https'), (':authority', 'api.example.com'), ('content-type', 'application/json'), ('content-length', '13'), ], body=b'{"key":"val"}' ) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('192.168.1.1', 54321)) assert req.method == 'POST' assert req.body.read() == b'{"key":"val"}' assert req.content_type == 'application/json' assert req.content_length == 13 def test_path_with_query_string(self): stream = self._make_stream([ (':method', 'GET'), (':path', '/search?q=test&page=1'), (':scheme', 'https'), (':authority', 'example.com'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.path == '/search' assert req.query == 'q=test&page=1' assert req.uri == '/search?q=test&page=1' def test_path_with_fragment(self): stream = self._make_stream([ (':method', 'GET'), (':path', '/page#section'), (':scheme', 'https'), (':authority', 'example.com'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.path == '/page' assert req.fragment == 'section' def test_headers_uppercase_conversion(self): """HTTP/2 headers are lowercase, should be converted to uppercase.""" stream = self._make_stream([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ('content-type', 'text/html'), ('accept-language', 'en-US'), ('x-custom-header', 'custom-value'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) header_names = [h[0] for h in req.headers] assert 'CONTENT-TYPE' in header_names assert 'ACCEPT-LANGUAGE' in header_names assert 'X-CUSTOM-HEADER' in header_names def test_host_header_from_authority(self): """Host header should be generated from :authority pseudo-header.""" stream = self._make_stream([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'test.example.com:8080'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) host = req.get_header('HOST') assert host == 'test.example.com:8080' def test_authority_overrides_host_header(self): """:authority MUST override Host header per RFC 9113 section 8.3.1.""" stream = self._make_stream([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'authority.example.com'), ('host', 'explicit.example.com'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) # Count HOST headers - should be exactly one, from :authority host_headers = [h for h in req.headers if h[0] == 'HOST'] assert len(host_headers) == 1 assert host_headers[0][1] == 'authority.example.com' def test_get_header_case_insensitive(self): stream = self._make_stream([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ('x-test-header', 'test-value'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.get_header('X-TEST-HEADER') == 'test-value' assert req.get_header('x-test-header') == 'test-value' assert req.get_header('X-Test-Header') == 'test-value' def test_get_header_not_found(self): stream = self._make_stream([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.get_header('X-Not-Exists') is None def test_content_length_property(self): stream = self._make_stream([ (':method', 'POST'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ('content-length', '42'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.content_length == 42 def test_content_length_none_when_missing(self): stream = self._make_stream([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.content_length is None def test_content_length_invalid_value(self): stream = self._make_stream([ (':method', 'POST'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ('content-length', 'not-a-number'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.content_length is None def test_content_type_property(self): stream = self._make_stream([ (':method', 'POST'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ('content-type', 'application/json; charset=utf-8'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.content_type == 'application/json; charset=utf-8' def test_content_type_none_when_missing(self): stream = self._make_stream([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.content_type is None class TestHTTP2RequestConnectionState: """Test connection state methods.""" def _make_stream(self, headers): conn = MockConnection() stream = HTTP2Stream(stream_id=1, connection=conn) stream.receive_headers(headers, end_stream=True) return stream def test_should_close_default_false(self): """HTTP/2 connections are persistent by default.""" stream = self._make_stream([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.should_close() is False def test_force_close(self): stream = self._make_stream([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) req.force_close() assert req.should_close() is True assert req.must_close is True class TestHTTP2RequestTrailers: """Test request trailers handling.""" def test_no_trailers(self): conn = MockConnection() stream = HTTP2Stream(stream_id=1, connection=conn) stream.receive_headers([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ], end_stream=True) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.trailers == [] def test_with_trailers(self): conn = MockConnection() stream = HTTP2Stream(stream_id=1, connection=conn) stream.receive_headers([ (':method', 'POST'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ], end_stream=False) stream.state = stream.state # Keep state stream.trailers = [ ('grpc-status', '0'), ('grpc-message', 'OK'), ] cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert len(req.trailers) == 2 assert ('GRPC-STATUS', '0') in req.trailers assert ('GRPC-MESSAGE', 'OK') in req.trailers class TestHTTP2RequestMetadata: """Test request metadata properties.""" def _make_stream(self, headers, stream_id=1): conn = MockConnection() stream = HTTP2Stream(stream_id=stream_id, connection=conn) stream.receive_headers(headers, end_stream=True) return stream def test_version_is_http2(self): stream = self._make_stream([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.version == (2, 0) def test_req_number_is_stream_id(self): stream = self._make_stream([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ], stream_id=5) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.req_number == 5 def test_peer_addr(self): stream = self._make_stream([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('10.0.0.1', 54321)) assert req.peer_addr == ('10.0.0.1', 54321) assert req.remote_addr == ('10.0.0.1', 54321) def test_proxy_protocol_info_none(self): """HTTP/2 doesn't use proxy protocol through data stream.""" stream = self._make_stream([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ]) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.proxy_protocol_info is None class TestHTTP2RequestRepr: """Test request string representation.""" def test_repr_format(self): conn = MockConnection() stream = HTTP2Stream(stream_id=3, connection=conn) stream.receive_headers([ (':method', 'POST'), (':path', '/api/users'), (':scheme', 'https'), (':authority', 'example.com'), ], end_stream=True) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) repr_str = repr(req) assert "HTTP2Request" in repr_str assert "method=POST" in repr_str assert "path=/api/users" in repr_str assert "stream_id=3" in repr_str class TestHTTP2RequestDefaults: """Test default values when pseudo-headers are missing.""" def test_default_method(self): conn = MockConnection() stream = HTTP2Stream(stream_id=1, connection=conn) stream.receive_headers([ (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ], end_stream=True) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.method == 'GET' def test_default_scheme(self): conn = MockConnection() stream = HTTP2Stream(stream_id=1, connection=conn) stream.receive_headers([ (':method', 'GET'), (':path', '/'), (':authority', 'example.com'), ], end_stream=True) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.scheme == 'https' def test_default_path(self): conn = MockConnection() stream = HTTP2Stream(stream_id=1, connection=conn) stream.receive_headers([ (':method', 'GET'), (':scheme', 'https'), (':authority', 'example.com'), ], end_stream=True) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.uri == '/' assert req.path == '/' class TestHTTP2RequestPriority: """Test HTTP2Request priority attributes.""" def test_default_priority_values(self): """Test that request inherits default stream priority.""" conn = MockConnection() stream = HTTP2Stream(stream_id=1, connection=conn) stream.receive_headers([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ], end_stream=True) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.priority_weight == 16 assert req.priority_depends_on == 0 def test_custom_priority_values(self): """Test that request inherits custom stream priority.""" conn = MockConnection() stream = HTTP2Stream(stream_id=3, connection=conn) # Update priority before creating request stream.update_priority(weight=200, depends_on=1) stream.receive_headers([ (':method', 'POST'), (':path', '/api/data'), (':scheme', 'https'), (':authority', 'example.com'), ], end_stream=False) stream.receive_data(b'{"data": "test"}', end_stream=True) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('192.168.1.100', 54321)) assert req.priority_weight == 200 assert req.priority_depends_on == 1 def test_priority_reflects_stream_at_request_creation(self): """Test that priority reflects stream state when request is created.""" conn = MockConnection() stream = HTTP2Stream(stream_id=1, connection=conn) stream.receive_headers([ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'example.com'), ], end_stream=True) cfg = MockConfig() # Create request with default priority req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) assert req.priority_weight == 16 # Update stream priority after request was created stream.update_priority(weight=256) # Request should still have old value (captured at creation time) assert req.priority_weight == 16 # Stream has new value assert stream.priority_weight == 256 class MockWSGIConfig: """Mock gunicorn configuration with WSGI-required attributes.""" def __init__(self): self.errorlog = '-' self.workers = 1 class TestHTTP2RequestWSGIEnviron: """Test HTTP/2 priority in WSGI environ.""" def test_priority_in_wsgi_environ(self): """Test that HTTP/2 priority is added to WSGI environ.""" from unittest import mock from gunicorn.http.wsgi import create conn = MockConnection() stream = HTTP2Stream(stream_id=1, connection=conn) stream.update_priority(weight=128, depends_on=3) stream.receive_headers([ (':method', 'GET'), (':path', '/test'), (':scheme', 'https'), (':authority', 'example.com'), ], end_stream=True) cfg = MockConfig() req = HTTP2Request(stream, cfg, ('127.0.0.1', 12345)) # Create a mock socket mock_sock = mock.Mock() mock_sock.getsockname.return_value = ('127.0.0.1', 8443) # Use WSGI config for environ creation wsgi_cfg = MockWSGIConfig() # Create WSGI environ resp, environ = create(req, mock_sock, ('127.0.0.1', 12345), ('127.0.0.1', 8443), wsgi_cfg) # Verify priority is in environ assert environ.get('gunicorn.http2.priority_weight') == 128 assert environ.get('gunicorn.http2.priority_depends_on') == 3 def test_priority_not_in_environ_for_http1(self): """Test that HTTP/1 requests don't have priority keys.""" from unittest import mock from gunicorn.http.wsgi import create # Create a mock HTTP/1 request (no priority attributes) mock_req = mock.Mock() mock_req.headers = [('HOST', 'example.com')] mock_req.scheme = 'https' mock_req.path = '/test' mock_req.query = '' mock_req.fragment = '' mock_req.method = 'GET' mock_req.uri = '/test' mock_req.version = (1, 1) mock_req._expected_100_continue = False mock_req.proxy_protocol_info = None mock_req.body = mock.Mock() # Remove priority attributes to simulate HTTP/1 request del mock_req.priority_weight del mock_req.priority_depends_on wsgi_cfg = MockWSGIConfig() mock_sock = mock.Mock() mock_sock.getsockname.return_value = ('127.0.0.1', 8443) resp, environ = create(mock_req, mock_sock, ('127.0.0.1', 12345), ('127.0.0.1', 8443), wsgi_cfg) # HTTP/1 requests should not have priority keys assert 'gunicorn.http2.priority_weight' not in environ assert 'gunicorn.http2.priority_depends_on' not in environ