99
1010import socket
1111import threading
12- import time
12+
13+ from hyperframe .frame import Frame , SettingsFrame
1314
1415from localstack_extensions .utils .h2_proxy import (
1516 get_frames_from_http2_stream ,
1819)
1920
2021
22+ def parse_server_frames (data : bytes ) -> list :
23+ """Parse HTTP/2 frames from server response data (no preface expected).
24+
25+ Server responses don't include the HTTP/2 preface - they start with frames directly.
26+ This function parses raw frame data using hyperframe directly.
27+ """
28+ frames = []
29+ pos = 0
30+ while pos + 9 <= len (data ): # Frame header is 9 bytes
31+ try :
32+ frame , length = Frame .parse_frame_header (memoryview (data [pos :pos + 9 ]))
33+ if pos + 9 + length > len (data ):
34+ break # Incomplete frame
35+ frame .parse_body (memoryview (data [pos + 9 :pos + 9 + length ]))
36+ frames .append (frame )
37+ pos += 9 + length
38+ except Exception :
39+ break
40+ return frames
41+
42+
2143# HTTP/2 connection preface
2244HTTP2_PREFACE = b"PRI * HTTP/2.0\r \n \r \n SM\r \n \r \n "
2345
2850class TestGrpcConnectivity :
2951 """Tests for basic HTTP/2 connectivity to grpcbin."""
3052
31- def test_tcp_connect_to_grpcbin (self , grpcbin_host , grpcbin_insecure_port ):
32- """Test that we can establish a TCP connection (no exception = success)."""
33- sock = socket .socket (socket .AF_INET , socket .SOCK_STREAM )
34- sock .settimeout (5.0 )
53+ def test_http2_connect_to_grpcbin (self , grpcbin_host , grpcbin_insecure_port ):
54+ """Test that we can establish an HTTP/2 connection and receive SETTINGS."""
55+ forwarder = TcpForwarder (port = grpcbin_insecure_port , host = grpcbin_host )
56+ received_data = []
57+ done = threading .Event ()
58+
59+ def callback (data ):
60+ received_data .append (data )
61+ done .set ()
62+
3563 try :
36- sock .connect ((grpcbin_host , grpcbin_insecure_port ))
64+ receive_thread = threading .Thread (
65+ target = forwarder .receive_loop , args = (callback ,), daemon = True
66+ )
67+ receive_thread .start ()
68+
69+ forwarder .send (HTTP2_PREFACE + SETTINGS_FRAME )
70+ done .wait (timeout = 5.0 )
71+
72+ # Should receive at least one response
73+ assert len (received_data ) > 0 , "Should receive server response"
3774 finally :
38- sock .close ()
75+ forwarder .close ()
3976
4077
4178class TestHttp2FrameCapture :
@@ -46,30 +83,32 @@ def test_capture_settings_frame(self, grpcbin_host, grpcbin_insecure_port):
4683 forwarder = TcpForwarder (port = grpcbin_insecure_port , host = grpcbin_host )
4784 received_data = []
4885 done = threading .Event ()
86+ thread_started = threading .Event ()
4987
5088 def callback (data ):
5189 received_data .append (data )
5290 done .set ()
5391
92+ def receive_with_signal ():
93+ thread_started .set ()
94+ forwarder .receive_loop (callback )
95+
5496 try :
55- receive_thread = threading .Thread (
56- target = forwarder .receive_loop , args = (callback ,), daemon = True
57- )
97+ receive_thread = threading .Thread (target = receive_with_signal , daemon = True )
5898 receive_thread .start ()
99+ thread_started .wait (timeout = 1.0 ) # Wait for receive thread to be ready
59100
60- forwarder .send (HTTP2_PREFACE )
101+ forwarder .send (HTTP2_PREFACE + SETTINGS_FRAME )
61102 done .wait (timeout = 5.0 )
62103
63- # Parse the response using our utilities
64- full_data = HTTP2_PREFACE + b"" .join (received_data )
65- frames = list ( get_frames_from_http2_stream ( full_data ) )
104+ # Parse the server response (no preface expected in server data)
105+ server_data = b"" .join (received_data )
106+ frames = parse_server_frames ( server_data )
66107
67108 # Check that we got frames
68109 assert len (frames ) > 0
69110
70111 # First frame should be SETTINGS
71- from hyperframe .frame import SettingsFrame
72-
73112 settings_frames = [f for f in frames if isinstance (f , SettingsFrame )]
74113 assert len (settings_frames ) > 0 , "Should receive at least one SETTINGS frame"
75114 finally :
@@ -80,24 +119,26 @@ def test_parse_server_settings(self, grpcbin_host, grpcbin_insecure_port):
80119 forwarder = TcpForwarder (port = grpcbin_insecure_port , host = grpcbin_host )
81120 received_data = []
82121 done = threading .Event ()
122+ thread_started = threading .Event ()
83123
84124 def callback (data ):
85125 received_data .append (data )
86126 done .set ()
87127
128+ def receive_with_signal ():
129+ thread_started .set ()
130+ forwarder .receive_loop (callback )
131+
88132 try :
89- receive_thread = threading .Thread (
90- target = forwarder .receive_loop , args = (callback ,), daemon = True
91- )
133+ receive_thread = threading .Thread (target = receive_with_signal , daemon = True )
92134 receive_thread .start ()
135+ thread_started .wait (timeout = 1.0 ) # Wait for receive thread to be ready
93136
94- forwarder .send (HTTP2_PREFACE )
137+ forwarder .send (HTTP2_PREFACE + SETTINGS_FRAME )
95138 done .wait (timeout = 5.0 )
96139
97- full_data = HTTP2_PREFACE + b"" .join (received_data )
98- frames = list (get_frames_from_http2_stream (full_data ))
99-
100- from hyperframe .frame import SettingsFrame
140+ server_data = b"" .join (received_data )
141+ frames = parse_server_frames (server_data )
101142
102143 settings_frames = [f for f in frames if isinstance (f , SettingsFrame )]
103144 assert len (settings_frames ) > 0
@@ -160,21 +201,13 @@ def callback(data):
160201 )
161202 receive_thread .start ()
162203
163- # Step 1: Send preface
164- forwarder .send (HTTP2_PREFACE )
165-
166- # Wait for server response
204+ # Send preface and SETTINGS frame together
205+ forwarder .send (HTTP2_PREFACE + SETTINGS_FRAME )
167206 first_response .wait (timeout = 5.0 )
168207
169- # Step 2: Send empty SETTINGS
170- forwarder .send (SETTINGS_FRAME )
171-
172- # Give server time to respond
173- time .sleep (0.2 )
174-
175- # Parse all frames
176- full_data = HTTP2_PREFACE + b"" .join (received_data )
177- frames = list (get_frames_from_http2_stream (full_data ))
208+ # Parse server response frames
209+ server_data = b"" .join (received_data )
210+ frames = parse_server_frames (server_data )
178211
179212 assert len (frames ) >= 1 , "Should receive at least one frame from server"
180213
@@ -201,16 +234,14 @@ def callback(data):
201234 )
202235 receive_thread .start ()
203236
204- forwarder .send (HTTP2_PREFACE )
237+ forwarder .send (HTTP2_PREFACE + SETTINGS_FRAME )
205238 done .wait (timeout = 5.0 )
206239
207- full_data = HTTP2_PREFACE + b"" .join (received_data )
208-
209- frames = list (get_frames_from_http2_stream (full_data ))
240+ server_data = b"" .join (received_data )
241+ frames = parse_server_frames (server_data )
210242 headers = get_headers_from_frames (frames )
211243
212- # Server's initial response typically doesn't include HEADERS frames
213- # (just SETTINGS), so headers will be empty - but the function should work
244+ # Server response has SETTINGS, not HEADERS, so headers will be empty
214245 assert headers is not None
215246 finally :
216247 forwarder .close ()
0 commit comments