Benoit Chesneau 95b7ffeeaa chore: prepare release 25.0.2
- Bump version to 25.0.2
- Update copyright year to 2026 in LICENSE and NOTICE
- Add license headers to all Python source files
- Add changelog entry for 25.0.2
2026-02-06 08:21:18 +01:00

296 lines
8.6 KiB
Python

#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
#!/usr/bin/env python
"""
Test script for HTTP/2 features example.
This script tests:
- HTTP/2 connection establishment
- Stream priority access
- Response trailers
Run the server first:
docker compose up --build
Then run tests:
python test_http2.py
Or run directly against local server:
python test_http2.py --url https://localhost:8443
"""
import argparse
import json
import ssl
import socket
import sys
from urllib.parse import urlparse
def create_h2_connection(host, port):
"""Create an HTTP/2 connection using the h2 library."""
try:
import h2.connection
import h2.config
except ImportError:
print("Please install h2: pip install h2")
sys.exit(1)
# Create socket with SSL
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx.set_alpn_protocols(['h2'])
sock = ctx.wrap_socket(sock, server_hostname=host)
sock.connect((host, port))
sock.settimeout(10.0)
# Verify ALPN
alpn = sock.selected_alpn_protocol()
if alpn != 'h2':
raise RuntimeError(f"HTTP/2 not negotiated, got: {alpn}")
# Create h2 connection
config = h2.config.H2Configuration(client_side=True)
h2_conn = h2.connection.H2Connection(config=config)
h2_conn.initiate_connection()
sock.sendall(h2_conn.data_to_send())
# Receive server settings
data = sock.recv(65536)
h2_conn.receive_data(data)
sock.sendall(h2_conn.data_to_send())
return sock, h2_conn
def h2_request(sock, h2_conn, stream_id, method, path, authority):
"""Make an HTTP/2 request and return the response."""
import h2.events
# Send request
h2_conn.send_headers(stream_id, [
(':method', method),
(':path', path),
(':authority', authority),
(':scheme', 'https'),
], end_stream=True)
sock.sendall(h2_conn.data_to_send())
# Collect response
status = None
headers = {}
body = b''
trailers = {}
while True:
data = sock.recv(65536)
if not data:
break
events = h2_conn.receive_data(data)
to_send = h2_conn.data_to_send()
if to_send:
sock.sendall(to_send)
for event in events:
if isinstance(event, h2.events.ResponseReceived):
if event.stream_id == stream_id:
for name, value in event.headers:
if name == b':status':
status = int(value.decode())
else:
headers[name.decode()] = value.decode()
elif isinstance(event, h2.events.DataReceived):
if event.stream_id == stream_id:
body += event.data
elif isinstance(event, h2.events.TrailersReceived):
if event.stream_id == stream_id:
for name, value in event.headers:
trailers[name.decode()] = value.decode()
elif isinstance(event, h2.events.StreamEnded):
if event.stream_id == stream_id:
return {
'status': status,
'headers': headers,
'body': body,
'trailers': trailers,
}
elif isinstance(event, h2.events.ConnectionTerminated):
raise RuntimeError(f"Connection terminated: {event.error_code}")
return None
def test_http2_connection(host, port):
"""Test that HTTP/2 is negotiated."""
print("\n=== Testing HTTP/2 Connection ===")
try:
sock, h2_conn = create_h2_connection(host, port)
print("HTTP/2 connection established successfully!")
response = h2_request(sock, h2_conn, 1, 'GET', '/', f'{host}:{port}')
print(f"Status: {response['status']}")
data = json.loads(response['body'].decode())
print(f"Extensions available: {data.get('extensions', [])}")
sock.close()
return response['status'] == 200
except Exception as e:
print(f"ERROR: {e}")
return False
def test_priority(host, port):
"""Test stream priority endpoint."""
print("\n=== Testing Stream Priority ===")
try:
sock, h2_conn = create_h2_connection(host, port)
response = h2_request(sock, h2_conn, 1, 'GET', '/priority', f'{host}:{port}')
print(f"Status: {response['status']}")
data = json.loads(response['body'].decode())
print(f"Priority info: {data.get('priority')}")
if data.get("priority"):
print(f" Weight: {data['priority']['weight']}")
print(f" Depends on: {data['priority']['depends_on']}")
sock.close()
return response['status'] == 200 and data.get("priority") is not None
except Exception as e:
print(f"ERROR: {e}")
return False
def test_trailers(host, port):
"""Test response trailers."""
print("\n=== Testing Response Trailers ===")
try:
sock, h2_conn = create_h2_connection(host, port)
response = h2_request(sock, h2_conn, 1, 'GET', '/trailers', f'{host}:{port}')
print(f"Status: {response['status']}")
print(f"Headers: {response['headers']}")
if response['trailers']:
print(f"Trailers received: {response['trailers']}")
if 'content-md5' in response['trailers']:
print(f" Content-MD5: {response['trailers']['content-md5']}")
else:
print("Note: No trailers received (client may not have advertised support)")
sock.close()
return response['status'] == 200
except Exception as e:
print(f"ERROR: {e}")
return False
def test_combined(host, port):
"""Test combined priority and trailers."""
print("\n=== Testing Combined Features ===")
try:
sock, h2_conn = create_h2_connection(host, port)
response = h2_request(sock, h2_conn, 1, 'GET', '/combined', f'{host}:{port}')
print(f"Status: {response['status']}")
data = json.loads(response['body'].decode())
print(f"Response: {json.dumps(data, indent=2)}")
if response['trailers']:
print(f"Trailers: {response['trailers']}")
sock.close()
return response['status'] == 200
except Exception as e:
print(f"ERROR: {e}")
return False
def test_multiple_streams(host, port):
"""Test multiple requests on the same connection."""
print("\n=== Testing Multiple Streams ===")
try:
sock, h2_conn = create_h2_connection(host, port)
# Make multiple requests on the same connection
paths = ['/', '/priority', '/trailers', '/combined']
for i, path in enumerate(paths):
stream_id = i * 2 + 1 # Odd numbers for client-initiated streams
response = h2_request(sock, h2_conn, stream_id, 'GET', path, f'{host}:{port}')
print(f" {path}: {response['status']}")
sock.close()
return True
except Exception as e:
print(f"ERROR: {e}")
return False
def main():
parser = argparse.ArgumentParser(description="Test HTTP/2 features")
parser.add_argument(
"--url",
default="https://localhost:8443",
help="Base URL of the server (default: https://localhost:8443)"
)
args = parser.parse_args()
parsed = urlparse(args.url)
host = parsed.hostname or 'localhost'
port = parsed.port or 8443
print(f"Testing against: {host}:{port}")
results = []
try:
results.append(("HTTP/2 Connection", test_http2_connection(host, port)))
results.append(("Stream Priority", test_priority(host, port)))
results.append(("Response Trailers", test_trailers(host, port)))
results.append(("Combined Features", test_combined(host, port)))
results.append(("Multiple Streams", test_multiple_streams(host, port)))
except ConnectionRefusedError:
print(f"\nConnection refused to {host}:{port}")
print("Make sure the server is running: docker compose up --build")
return 1
except Exception as e:
print(f"\nUnexpected error: {e}")
return 1
print("\n=== Test Results ===")
all_passed = True
for name, passed in results:
status = "PASS" if passed else "FAIL"
print(f" {name}: {status}")
if not passed:
all_passed = False
if all_passed:
print("\nAll tests passed!")
return 0
else:
print("\nSome tests failed.")
return 1
if __name__ == "__main__":
sys.exit(main())