# -*- coding: utf-8 - # # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. """Tests for HTTP/2 server connection.""" import pytest from unittest import mock from io import BytesIO # Check if h2 is available for integration tests try: import h2.connection import h2.config import h2.events import h2.exceptions H2_AVAILABLE = True except ImportError: H2_AVAILABLE = False from gunicorn.http2.errors import ( HTTP2Error, HTTP2ConnectionError ) pytestmark = pytest.mark.skipif(not H2_AVAILABLE, reason="h2 library not available") class MockConfig: """Mock gunicorn configuration for HTTP/2.""" def __init__(self): self.http2_max_concurrent_streams = 100 self.http2_initial_window_size = 65535 self.http2_max_frame_size = 16384 self.http2_max_header_list_size = 65536 class MockSocket: """Mock socket for testing connection without real network I/O.""" def __init__(self, data=b''): self._recv_buffer = BytesIO(data) self._sent = bytearray() self._closed = False def recv(self, size): return self._recv_buffer.read(size) def sendall(self, data): if self._closed: raise OSError("Socket is closed") self._sent.extend(data) def close(self): self._closed = True def get_sent_data(self): return bytes(self._sent) def set_recv_data(self, data): self._recv_buffer = BytesIO(data) def create_client_connection(): """Create an h2 client connection for generating test frames.""" config = h2.config.H2Configuration(client_side=True) conn = h2.connection.H2Connection(config=config) conn.initiate_connection() return conn class TestHTTP2ServerConnectionInit: """Test HTTP2ServerConnection initialization.""" def test_basic_initialization(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) assert conn.cfg is cfg assert conn.sock is sock assert conn.client_addr == ('127.0.0.1', 12345) assert conn.streams == {} assert conn.is_closed is False assert conn._initialized is False def test_settings_from_config(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() cfg.http2_max_concurrent_streams = 50 cfg.http2_initial_window_size = 32768 sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) assert conn.max_concurrent_streams == 50 assert conn.initial_window_size == 32768 class TestHTTP2ServerConnectionInitiate: """Test connection initiation.""" def test_initiate_connection(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() assert conn._initialized is True # Should have sent settings frame sent_data = sock.get_sent_data() assert len(sent_data) > 0 def test_initiate_connection_idempotent(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() first_sent = len(sock.get_sent_data()) conn.initiate_connection() # Second call second_sent = len(sock.get_sent_data()) # Should not send additional data assert first_sent == second_sent class TestHTTP2ServerConnectionReceiveData: """Test receiving and processing data.""" def test_receive_empty_data_closes_connection(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket(b'') conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() requests = conn.receive_data() assert conn.is_closed is True assert requests == [] def test_receive_client_preface_and_headers(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Generate client data client = create_client_connection() client_preface = client.data_to_send() # Simulate server receiving client settings # Feed client preface to server requests = conn.receive_data(client_preface) # No requests yet, just settings exchange assert requests == [] def test_receive_simple_get_request(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Create client and send request client = create_client_connection() client_preface = client.data_to_send() # Process client preface on server conn.receive_data(client_preface) # Server may have sent settings, feed them to client server_data = sock.get_sent_data() if server_data: client.receive_data(server_data) # Client sends GET request client.send_headers( stream_id=1, headers=[ (':method', 'GET'), (':path', '/test'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True ) request_data = client.data_to_send() # Server receives request requests = conn.receive_data(request_data) assert len(requests) == 1 req = requests[0] assert req.method == 'GET' assert req.path == '/test' def test_receive_post_with_body(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Create client client = create_client_connection() client_preface = client.data_to_send() conn.receive_data(client_preface) server_data = sock.get_sent_data() if server_data: client.receive_data(server_data) # Client sends POST with body client.send_headers( stream_id=1, headers=[ (':method', 'POST'), (':path', '/submit'), (':scheme', 'https'), (':authority', 'localhost'), ('content-type', 'application/json'), ('content-length', '13'), ], end_stream=False ) client.send_data(stream_id=1, data=b'{"key":"val"}', end_stream=True) request_data = client.data_to_send() requests = conn.receive_data(request_data) assert len(requests) == 1 req = requests[0] assert req.method == 'POST' assert req.body.read() == b'{"key":"val"}' def test_socket_error_raises_connection_error(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = mock.Mock() sock.recv.side_effect = OSError("Connection reset") conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() with pytest.raises(HTTP2ConnectionError): conn.receive_data() class TestHTTP2ServerConnectionSendResponse: """Test sending responses.""" def test_send_simple_response(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Create a stream by receiving a request client = create_client_connection() client_preface = client.data_to_send() conn.receive_data(client_preface) server_data = sock.get_sent_data() if server_data: client.receive_data(server_data) client.send_headers(1, [ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True) conn.receive_data(client.data_to_send()) # Send response sock._sent.clear() conn.send_response( stream_id=1, status=200, headers=[('content-type', 'text/plain')], body=b'Hello!' ) sent = sock.get_sent_data() assert len(sent) > 0 # Verify client receives valid response events = client.receive_data(sent) response_events = [e for e in events if isinstance(e, h2.events.ResponseReceived)] data_events = [e for e in events if isinstance(e, h2.events.DataReceived)] assert len(response_events) == 1 assert len(data_events) == 1 assert data_events[0].data == b'Hello!' def test_send_response_with_empty_body(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() client = create_client_connection() conn.receive_data(client.data_to_send()) client.receive_data(sock.get_sent_data()) client.send_headers(1, [ (':method', 'HEAD'), (':path', '/'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True) conn.receive_data(client.data_to_send()) sock._sent.clear() conn.send_response(stream_id=1, status=200, headers=[], body=None) events = client.receive_data(sock.get_sent_data()) stream_ended = [e for e in events if isinstance(e, h2.events.StreamEnded)] assert len(stream_ended) == 1 def test_send_response_invalid_stream(self): """Test that sending response on invalid stream returns False.""" from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Sending to a non-existent stream should return False gracefully result = conn.send_response(stream_id=999, status=200, headers=[], body=None) assert result is False class TestHTTP2ServerConnectionSendError: """Test sending error responses.""" def test_send_error_with_message(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() client = create_client_connection() conn.receive_data(client.data_to_send()) client.receive_data(sock.get_sent_data()) client.send_headers(1, [ (':method', 'GET'), (':path', '/notfound'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True) conn.receive_data(client.data_to_send()) sock._sent.clear() conn.send_error(stream_id=1, status_code=404, message="Not Found") events = client.receive_data(sock.get_sent_data()) response_events = [e for e in events if isinstance(e, h2.events.ResponseReceived)] data_events = [e for e in events if isinstance(e, h2.events.DataReceived)] assert len(response_events) == 1 # h2 library returns headers as list of tuples, convert to dict # Note: headers may be bytes or strings depending on h2 version headers_list = response_events[0].headers status = None for name, value in headers_list: name_str = name.decode() if isinstance(name, bytes) else name if name_str == ':status': status = value.decode() if isinstance(value, bytes) else value break assert status == '404' assert len(data_events) == 1 assert data_events[0].data == b"Not Found" class TestHTTP2ServerConnectionResetStream: """Test stream reset.""" def test_reset_stream(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() client = create_client_connection() conn.receive_data(client.data_to_send()) client.receive_data(sock.get_sent_data()) client.send_headers(1, [ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=False) conn.receive_data(client.data_to_send()) sock._sent.clear() conn.reset_stream(stream_id=1, error_code=0x8) # CANCEL events = client.receive_data(sock.get_sent_data()) reset_events = [e for e in events if isinstance(e, h2.events.StreamReset)] assert len(reset_events) == 1 assert reset_events[0].error_code == 0x8 class TestHTTP2ServerConnectionClose: """Test connection close.""" def test_close_connection(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() client = create_client_connection() conn.receive_data(client.data_to_send()) sock._sent.clear() conn.close() assert conn.is_closed is True # Should have sent GOAWAY events = client.receive_data(sock.get_sent_data()) goaway_events = [e for e in events if isinstance(e, h2.events.ConnectionTerminated)] assert len(goaway_events) == 1 def test_close_idempotent(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() conn.close() sent_after_first = len(sock.get_sent_data()) conn.close() # Second call sent_after_second = len(sock.get_sent_data()) # Should not send additional GOAWAY assert sent_after_first == sent_after_second class TestHTTP2ServerConnectionCleanup: """Test stream cleanup.""" def test_cleanup_stream(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() client = create_client_connection() conn.receive_data(client.data_to_send()) client.receive_data(sock.get_sent_data()) client.send_headers(1, [ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True) conn.receive_data(client.data_to_send()) assert 1 in conn.streams conn.cleanup_stream(1) assert 1 not in conn.streams def test_cleanup_nonexistent_stream(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Should not raise conn.cleanup_stream(999) class TestHTTP2ServerConnectionMultipleStreams: """Test handling multiple concurrent streams.""" def test_multiple_streams(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() client = create_client_connection() conn.receive_data(client.data_to_send()) client.receive_data(sock.get_sent_data()) # Send multiple requests client.send_headers(1, [ (':method', 'GET'), (':path', '/one'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True) client.send_headers(3, [ (':method', 'GET'), (':path', '/two'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True) requests = conn.receive_data(client.data_to_send()) assert len(requests) == 2 paths = {req.path for req in requests} assert paths == {'/one', '/two'} class TestHTTP2ServerConnectionRepr: """Test string representation.""" def test_repr(self): from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) repr_str = repr(conn) assert "HTTP2ServerConnection" in repr_str assert "streams=" in repr_str assert "closed=" in repr_str class TestHTTP2ServerConnectionPriority: """Test HTTP/2 priority handling.""" def test_handle_priority_updated_existing_stream(self): """Test handling priority update for existing stream.""" from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Create a client connection to generate frames client_conn = create_client_connection() # Get client preface client_data = client_conn.data_to_send() # Feed client preface to server conn.receive_data(client_data) sock._sent = bytearray() # Send a request to create a stream client_conn.send_headers(1, [ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'localhost'), ]) request_data = client_conn.data_to_send() conn.receive_data(request_data) # Verify stream was created assert 1 in conn.streams stream = conn.streams[1] # Default priority values assert stream.priority_weight == 16 assert stream.priority_depends_on == 0 # Send a PRIORITY frame client_conn.prioritize(1, weight=128, depends_on=0, exclusive=False) priority_data = client_conn.data_to_send() conn.receive_data(priority_data) # Verify priority was updated assert stream.priority_weight == 128 def test_handle_priority_updated_nonexistent_stream(self): """Test that priority update for nonexistent stream is ignored.""" from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Create a client connection client_conn = create_client_connection() client_data = client_conn.data_to_send() conn.receive_data(client_data) # Send a PRIORITY frame for a stream that doesn't exist # This should not raise an error client_conn.prioritize(99, weight=64, depends_on=0, exclusive=False) priority_data = client_conn.data_to_send() # Should not raise conn.receive_data(priority_data) class TestHTTP2ServerConnectionTrailers: """Test HTTP/2 response trailer support.""" def test_send_trailers_after_headers_and_body(self): """Test sending trailers after response headers and body.""" from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Create a client connection client_conn = create_client_connection() client_data = client_conn.data_to_send() conn.receive_data(client_data) sock._sent = bytearray() # Send a request client_conn.send_headers(1, [ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True) request_data = client_conn.data_to_send() conn.receive_data(request_data) # Manually send headers without ending stream (for trailer support) stream = conn.streams[1] response_headers = [(':status', '200'), ('content-type', 'text/plain')] conn.h2_conn.send_headers(1, response_headers, end_stream=False) stream.send_headers(response_headers, end_stream=False) conn._send_pending_data() # Send body without ending stream conn.h2_conn.send_data(1, b'Hello World', end_stream=False) stream.send_data(b'Hello World', end_stream=False) conn._send_pending_data() # Send trailers trailers = [('grpc-status', '0'), ('grpc-message', 'OK')] conn.send_trailers(1, trailers) # Verify stream is closed assert stream.response_complete is True assert stream.response_trailers == [('grpc-status', '0'), ('grpc-message', 'OK')] def test_send_trailers_pseudo_header_raises(self): """Test that pseudo-headers in trailers raise error.""" from gunicorn.http2.connection import HTTP2ServerConnection from gunicorn.http2.errors import HTTP2Error cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() client_conn = create_client_connection() client_data = client_conn.data_to_send() conn.receive_data(client_data) # Send a request client_conn.send_headers(1, [ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True) conn.receive_data(client_conn.data_to_send()) # Send response conn.send_response(1, 200, [('content-type', 'text/plain')], None) # Try to send trailers with pseudo-header with pytest.raises(HTTP2Error) as exc_info: conn.send_trailers(1, [(':status', '200')]) assert "Pseudo-header" in str(exc_info.value) def test_send_trailers_without_headers_returns_false(self): """Test that sending trailers without headers returns False.""" from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() client_conn = create_client_connection() client_data = client_conn.data_to_send() conn.receive_data(client_data) # Send a request client_conn.send_headers(1, [ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True) conn.receive_data(client_conn.data_to_send()) # Try to send trailers without sending headers first - should return False result = conn.send_trailers(1, [('trailer', 'value')]) assert result is False def test_send_trailers_nonexistent_stream_returns_false(self): """Test that sending trailers on nonexistent stream returns False.""" from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() client_conn = create_client_connection() conn.receive_data(client_conn.data_to_send()) # Sending trailers to non-existent stream should return False result = conn.send_trailers(99, [('trailer', 'value')]) assert result is False class TestHTTP2FlowControl: """Test HTTP/2 flow control handling.""" def test_send_data_respects_zero_window(self): """Test that send_data returns False when flow control window is 0.""" from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Create client and send preface client_conn = create_client_connection() conn.receive_data(client_conn.data_to_send()) # Send a request client_conn.send_headers(1, [ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True) conn.receive_data(client_conn.data_to_send()) # Send response headers without ending stream (pass body=b'' placeholder) # We need to send headers first, so use h2_conn directly conn.h2_conn.send_headers(1, [ (':status', '200'), ('content-type', 'text/plain'), ], end_stream=False) conn._send_pending_data() conn.streams[1].send_headers([(':status', '200')], end_stream=False) # Mock the flow control window to return 0 original_window = conn.h2_conn.local_flow_control_window conn.h2_conn.local_flow_control_window = lambda stream_id: 0 # Try to send data - should return False (not raise) result = conn.send_data(1, b'Hello, World!') assert result is False # Restore conn.h2_conn.local_flow_control_window = original_window def test_send_data_respects_flow_control(self): """Test that send_data chunks data according to flow control window.""" from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Create client and send preface client_conn = create_client_connection() conn.receive_data(client_conn.data_to_send()) # Send a request client_conn.send_headers(1, [ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True) conn.receive_data(client_conn.data_to_send()) # Send response headers without ending stream conn.h2_conn.send_headers(1, [ (':status', '200'), ('content-type', 'text/plain'), ], end_stream=False) conn._send_pending_data() conn.streams[1].send_headers([(':status', '200')], end_stream=False) # Send small data - should succeed within window small_data = b'Hello' conn.send_data(1, small_data, end_stream=True) # Verify data was sent sent_data = sock.get_sent_data() assert len(sent_data) > 0 class TestHTTP2StreamClosedHandling: """Test graceful handling of StreamClosedError.""" def test_send_response_on_closed_stream(self): """Test that send_response gracefully handles closed stream.""" from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Create client and send preface client_conn = create_client_connection() conn.receive_data(client_conn.data_to_send()) # Send a request client_conn.send_headers(1, [ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True) conn.receive_data(client_conn.data_to_send()) # Simulate client resetting the stream client_conn.reset_stream(1) conn.receive_data(client_conn.data_to_send()) # Try to send response - should return False, not raise result = conn.send_response(1, 200, [('content-type', 'text/plain')], b'Hello') assert result is False def test_send_data_on_reset_stream(self): """Test that send_data gracefully handles reset stream.""" from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Create client and send preface client_conn = create_client_connection() conn.receive_data(client_conn.data_to_send()) # Send a request client_conn.send_headers(1, [ (':method', 'GET'), (':path', '/'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True) conn.receive_data(client_conn.data_to_send()) # Send response headers without ending stream conn.h2_conn.send_headers(1, [ (':status', '200'), ('content-type', 'text/plain'), ], end_stream=False) conn._send_pending_data() conn.streams[1].send_headers([(':status', '200')], end_stream=False) # Simulate client resetting the stream client_conn.reset_stream(1) conn.receive_data(client_conn.data_to_send()) # Try to send data - should return False, not raise result = conn.send_data(1, b'Hello, World!', end_stream=True) assert result is False class TestHTTP2WindowOverflowHandling: """Test window overflow handling.""" def test_window_overflow_sends_goaway(self): """Test that window overflow results in GOAWAY with FLOW_CONTROL_ERROR.""" from gunicorn.http2.connection import HTTP2ServerConnection from gunicorn.http2.errors import HTTP2ErrorCode cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Create client and send preface client_conn = create_client_connection() conn.receive_data(client_conn.data_to_send()) # Mock increment_flow_control_window to raise ValueError (overflow) original_increment = conn.h2_conn.increment_flow_control_window def raise_overflow(increment, stream_id=None): raise ValueError("Flow control window too large") conn.h2_conn.increment_flow_control_window = raise_overflow # Send a request with data to trigger the overflow client_conn.send_headers(1, [ (':method', 'POST'), (':path', '/'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=False) client_conn.send_data(1, b'test data', end_stream=True) conn.receive_data(client_conn.data_to_send()) # Connection should be closed with FLOW_CONTROL_ERROR assert conn.is_closed is True class TestHTTP2ProtocolErrorHandling: """Test protocol error handling sends proper GOAWAY.""" def test_protocol_error_sends_goaway(self): """Test that protocol errors result in GOAWAY being sent.""" from gunicorn.http2.connection import HTTP2ServerConnection from gunicorn.http2.errors import HTTP2ProtocolError, HTTP2ErrorCode cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() # Create client and send preface client_conn = create_client_connection() conn.receive_data(client_conn.data_to_send()) # Clear sent data to only capture new frames sock._sent.clear() # Mock h2_conn.receive_data to raise ProtocolError def raise_protocol_error(data): raise h2.exceptions.ProtocolError("Test protocol error") conn.h2_conn.receive_data = raise_protocol_error # This should send GOAWAY and raise ProtocolError with pytest.raises(HTTP2ProtocolError) as exc_info: conn.receive_data(b'dummy data') assert "Test protocol error" in str(exc_info.value) # Verify something was sent (GOAWAY frame) sent_data = sock.get_sent_data() assert len(sent_data) > 0 # Connection should be marked as closed assert conn.is_closed is True class TestHTTP2StreamEndedBodyComplete: """Test that _handle_stream_ended sets _body_complete on the stream.""" def test_stream_ended_sets_body_complete(self): """_handle_stream_ended must set stream._body_complete = True.""" from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() client = create_client_connection() client_preface = client.data_to_send() conn.receive_data(client_preface) server_data = sock.get_sent_data() if server_data: client.receive_data(server_data) # Client sends POST with body (separate HEADERS and DATA frames) client.send_headers( stream_id=1, headers=[ (':method', 'POST'), (':path', '/test'), (':scheme', 'https'), (':authority', 'localhost'), ('content-type', 'application/json'), ], end_stream=False, ) client.send_data(stream_id=1, data=b'{"input": "test"}', end_stream=True) request_data = client.data_to_send() requests = conn.receive_data(request_data) assert len(requests) == 1 stream = conn.streams.get(1) assert stream is not None assert stream._body_complete is True assert stream.request_complete is True def test_stream_ended_signals_body_event(self): """_handle_stream_ended must signal _body_event if it exists.""" import asyncio from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() client = create_client_connection() client_preface = client.data_to_send() conn.receive_data(client_preface) server_data = sock.get_sent_data() if server_data: client.receive_data(server_data) # Client sends headers without end_stream to create the stream client.send_headers( stream_id=1, headers=[ (':method', 'POST'), (':path', '/test'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=False, ) headers_data = client.data_to_send() conn.receive_data(headers_data) # Manually set _body_event on the stream (simulates read_body_chunk # having been called, which lazy-inits the event) stream = conn.streams.get(1) assert stream is not None stream._body_event = asyncio.Event() # Now send data + end_stream client.send_data(stream_id=1, data=b'body', end_stream=True) request_data = client.data_to_send() conn.receive_data(request_data) assert stream._body_event.is_set() def test_stream_ended_without_body_event_does_not_raise(self): """_handle_stream_ended must not raise when _body_event is None.""" from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() client = create_client_connection() client_preface = client.data_to_send() conn.receive_data(client_preface) server_data = sock.get_sent_data() if server_data: client.receive_data(server_data) # Send GET with end_stream (no body, _body_event never initialised) client.send_headers( stream_id=1, headers=[ (':method', 'GET'), (':path', '/test'), (':scheme', 'https'), (':authority', 'localhost'), ], end_stream=True, ) request_data = client.data_to_send() # Should not raise even though _body_event is None requests = conn.receive_data(request_data) assert len(requests) == 1 @pytest.mark.asyncio async def test_h2_post_body_not_duplicated(self): """Full flow: streaming read must not re-read body from BytesIO. Simulates what the receive() closure in protocol.py does: 1. read_body_chunk() returns the body 2. read_body_chunk() returns None (body complete) 3. Total bytes received == original body length (not doubled) """ from gunicorn.http2.connection import HTTP2ServerConnection cfg = MockConfig() sock = MockSocket() conn = HTTP2ServerConnection(cfg, sock, ('127.0.0.1', 12345)) conn.initiate_connection() client = create_client_connection() client_preface = client.data_to_send() conn.receive_data(client_preface) server_data = sock.get_sent_data() if server_data: client.receive_data(server_data) body = b'{"input": ["hello world"]}' client.send_headers( stream_id=1, headers=[ (':method', 'POST'), (':path', '/embeddings'), (':scheme', 'https'), (':authority', 'localhost'), ('content-type', 'application/json'), ('content-length', str(len(body))), ], end_stream=False, ) client.send_data(stream_id=1, data=body, end_stream=True) request_data = client.data_to_send() requests = conn.receive_data(request_data) assert len(requests) == 1 stream = conn.streams.get(1) # Simulate what receive() does: read chunks via read_body_chunk() received = bytearray() while True: chunk = await stream.read_body_chunk() if chunk is None: break received.extend(chunk) # The critical assertion: body must not be duplicated assert bytes(received) == body assert len(received) == len(body) # _body_complete must be True so receive() knows to stop assert stream._body_complete is True # BytesIO must still have the data (for get_request_body compatibility) # but read_body_chunk returning None prevents the fast path in receive() # from ever being reached because body_received gets set to True class TestHTTP2NotAvailable: """Test behavior when h2 is not available.""" def test_import_error_raises_not_available(self): from gunicorn.http2 import errors # Test that HTTP2NotAvailable can be raised with pytest.raises(errors.HTTP2NotAvailable): raise errors.HTTP2NotAvailable()