mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-01 18:21:30 +08:00
Add complete HTTP/2 example in examples/http2_features/: - ASGI app showing priority access and trailer sending - Test script using raw h2 library for HTTP/2 testing - Docker setup for easy testing - Documentation update referencing the example The example demonstrates: - Reading http.response.priority extension in ASGI scope - Sending http.response.trailers messages - Multiple streams on the same connection
292 lines
8.5 KiB
Python
292 lines
8.5 KiB
Python
#!/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())
|