mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-01 10:11:30 +08:00
docs(http2): add ASGI example demonstrating priority and trailers
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
This commit is contained in:
parent
0f298e4838
commit
fa5e319f15
@ -476,6 +476,31 @@ with httpx.Client(http2=True, verify=False) as client:
|
||||
print(f"HTTP Version: {response.http_version}")
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
A complete HTTP/2 example demonstrating priority and trailers is available in the
|
||||
`examples/http2_features/` directory. This includes:
|
||||
|
||||
- **http2_app.py**: ASGI application showing priority access and trailer sending
|
||||
- **test_http2.py**: Test script verifying HTTP/2 features
|
||||
- **Dockerfile** and **docker-compose.yml**: Docker setup for testing
|
||||
|
||||
To run the example:
|
||||
|
||||
```bash
|
||||
cd examples/http2_features
|
||||
docker compose up --build
|
||||
|
||||
# In another terminal:
|
||||
docker compose exec http2-features python /app/http2_features/test_http2.py
|
||||
```
|
||||
|
||||
The example demonstrates:
|
||||
|
||||
1. **Priority access**: Reading `http.response.priority` extension in ASGI scope
|
||||
2. **Response trailers**: Sending `http.response.trailers` messages
|
||||
3. **Combined features**: Using both priority and trailers in one response
|
||||
|
||||
## See Also
|
||||
|
||||
- [Settings Reference](reference/settings.md#http2_max_concurrent_streams) - All HTTP/2 settings
|
||||
|
||||
22
examples/http2_features/Dockerfile
Normal file
22
examples/http2_features/Dockerfile
Normal file
@ -0,0 +1,22 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install h2 for HTTP/2 support and httpx for testing
|
||||
RUN pip install --no-cache-dir h2 httpx
|
||||
|
||||
# Copy gunicorn source and install
|
||||
COPY . /app/gunicorn-src
|
||||
RUN pip install /app/gunicorn-src
|
||||
|
||||
# Copy example app
|
||||
COPY examples/http2_features /app/http2_features
|
||||
|
||||
# Copy SSL certificates
|
||||
COPY examples/server.crt /app/certs/server.crt
|
||||
COPY examples/server.key /app/certs/server.key
|
||||
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
EXPOSE 8443
|
||||
CMD ["gunicorn", "http2_features.http2_app:app", "-c", "http2_features/gunicorn_conf.py"]
|
||||
3
examples/http2_features/__init__.py
Normal file
3
examples/http2_features/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
#
|
||||
# This file is part of gunicorn released under the MIT license.
|
||||
# See the NOTICE for more information.
|
||||
13
examples/http2_features/docker-compose.yml
Normal file
13
examples/http2_features/docker-compose.yml
Normal file
@ -0,0 +1,13 @@
|
||||
services:
|
||||
http2-features:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: examples/http2_features/Dockerfile
|
||||
ports:
|
||||
- "8443:8443"
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import httpx; httpx.get('https://127.0.0.1:8443/health', verify=False)"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 5s
|
||||
20
examples/http2_features/gunicorn_conf.py
Normal file
20
examples/http2_features/gunicorn_conf.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Gunicorn configuration for HTTP/2 features example
|
||||
|
||||
bind = "0.0.0.0:8443"
|
||||
workers = 2
|
||||
worker_class = "asgi"
|
||||
|
||||
# SSL configuration (required for HTTP/2)
|
||||
certfile = "/app/certs/server.crt"
|
||||
keyfile = "/app/certs/server.key"
|
||||
|
||||
# HTTP/2 configuration
|
||||
http_protocols = "h2,h1"
|
||||
http2_max_concurrent_streams = 100
|
||||
http2_initial_window_size = 65535
|
||||
http2_max_frame_size = 16384
|
||||
|
||||
# Logging
|
||||
accesslog = "-"
|
||||
errorlog = "-"
|
||||
loglevel = "info"
|
||||
270
examples/http2_features/http2_app.py
Normal file
270
examples/http2_features/http2_app.py
Normal file
@ -0,0 +1,270 @@
|
||||
#
|
||||
# This file is part of gunicorn released under the MIT license.
|
||||
# See the NOTICE for more information.
|
||||
|
||||
"""
|
||||
HTTP/2 ASGI application demonstrating priority and trailers.
|
||||
|
||||
This example shows how to:
|
||||
- Access stream priority information from HTTP/2 requests
|
||||
- Send response trailers (useful for gRPC, checksums, etc.)
|
||||
|
||||
Run with:
|
||||
cd examples/http2_features
|
||||
docker compose up --build
|
||||
|
||||
Test with:
|
||||
python test_http2.py
|
||||
|
||||
Or manually:
|
||||
curl -k --http2 https://localhost:8443/
|
||||
curl -k --http2 https://localhost:8443/priority
|
||||
curl -k --http2 https://localhost:8443/trailers
|
||||
"""
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
|
||||
|
||||
async def app(scope, receive, send):
|
||||
"""ASGI application demonstrating HTTP/2 priority and trailers."""
|
||||
|
||||
if scope["type"] == "lifespan":
|
||||
await handle_lifespan(scope, receive, send)
|
||||
elif scope["type"] == "http":
|
||||
await handle_http(scope, receive, send)
|
||||
else:
|
||||
raise ValueError(f"Unknown scope type: {scope['type']}")
|
||||
|
||||
|
||||
async def handle_lifespan(scope, receive, send):
|
||||
"""Handle lifespan events (startup/shutdown)."""
|
||||
while True:
|
||||
message = await receive()
|
||||
if message["type"] == "lifespan.startup":
|
||||
print("HTTP/2 features app starting...")
|
||||
await send({"type": "lifespan.startup.complete"})
|
||||
elif message["type"] == "lifespan.shutdown":
|
||||
print("HTTP/2 features app shutting down...")
|
||||
await send({"type": "lifespan.shutdown.complete"})
|
||||
return
|
||||
|
||||
|
||||
async def handle_http(scope, receive, send):
|
||||
"""Route HTTP requests to handlers."""
|
||||
path = scope["path"]
|
||||
method = scope["method"]
|
||||
|
||||
if path == "/" and method == "GET":
|
||||
await handle_index(scope, receive, send)
|
||||
elif path == "/priority" and method == "GET":
|
||||
await handle_priority(scope, receive, send)
|
||||
elif path == "/trailers" and method in ("GET", "POST"):
|
||||
await handle_trailers(scope, receive, send)
|
||||
elif path == "/combined" and method in ("GET", "POST"):
|
||||
await handle_combined(scope, receive, send)
|
||||
elif path == "/health" and method == "GET":
|
||||
await send_response(send, 200, b"OK")
|
||||
else:
|
||||
await send_response(send, 404, b"Not Found\n")
|
||||
|
||||
|
||||
async def handle_index(scope, receive, send):
|
||||
"""Show available endpoints and HTTP/2 features."""
|
||||
extensions = scope.get("extensions", {})
|
||||
http_version = scope.get("http_version", "1.1")
|
||||
|
||||
info = {
|
||||
"message": "HTTP/2 Features Demo",
|
||||
"http_version": http_version,
|
||||
"endpoints": {
|
||||
"/": "This info page",
|
||||
"/priority": "Shows stream priority information",
|
||||
"/trailers": "Demonstrates response trailers with checksum",
|
||||
"/combined": "Shows both priority and trailers",
|
||||
"/health": "Health check endpoint",
|
||||
},
|
||||
"extensions": list(extensions.keys()),
|
||||
}
|
||||
|
||||
body = json.dumps(info, indent=2).encode() + b"\n"
|
||||
await send_response(send, 200, body, content_type=b"application/json")
|
||||
|
||||
|
||||
async def handle_priority(scope, receive, send):
|
||||
"""Return stream priority information.
|
||||
|
||||
HTTP/2 allows clients to indicate relative importance of requests.
|
||||
Gunicorn exposes this through the http.response.priority extension.
|
||||
"""
|
||||
extensions = scope.get("extensions", {})
|
||||
priority_info = extensions.get("http.response.priority")
|
||||
|
||||
if priority_info:
|
||||
response = {
|
||||
"http_version": scope.get("http_version", "1.1"),
|
||||
"priority": {
|
||||
"weight": priority_info["weight"],
|
||||
"depends_on": priority_info["depends_on"],
|
||||
"description": (
|
||||
f"Weight {priority_info['weight']}/256 - "
|
||||
f"{'high' if priority_info['weight'] > 128 else 'normal' if priority_info['weight'] > 64 else 'low'} priority"
|
||||
),
|
||||
},
|
||||
"note": "Priority is advisory - use for scheduling hints",
|
||||
}
|
||||
else:
|
||||
response = {
|
||||
"http_version": scope.get("http_version", "1.1"),
|
||||
"priority": None,
|
||||
"note": "Priority information only available for HTTP/2 requests",
|
||||
}
|
||||
|
||||
body = json.dumps(response, indent=2).encode() + b"\n"
|
||||
await send_response(send, 200, body, content_type=b"application/json")
|
||||
|
||||
|
||||
async def handle_trailers(scope, receive, send):
|
||||
"""Demonstrate response trailers.
|
||||
|
||||
Trailers are headers sent after the response body.
|
||||
Common uses: gRPC status codes, checksums, timing info.
|
||||
"""
|
||||
extensions = scope.get("extensions", {})
|
||||
supports_trailers = "http.response.trailers" in extensions
|
||||
|
||||
# Read request body if POST
|
||||
body_data = b""
|
||||
if scope["method"] == "POST":
|
||||
body_data = await read_body(receive)
|
||||
|
||||
# Generate response
|
||||
response_body = body_data if body_data else b"Hello from HTTP/2 with trailers!\n"
|
||||
|
||||
# Calculate checksum for trailer
|
||||
checksum = hashlib.md5(response_body).hexdigest()
|
||||
|
||||
if supports_trailers:
|
||||
# Send response announcing trailers
|
||||
await send({
|
||||
"type": "http.response.start",
|
||||
"status": 200,
|
||||
"headers": [
|
||||
(b"content-type", b"application/octet-stream"),
|
||||
(b"trailer", b"content-md5, x-processing-time"),
|
||||
],
|
||||
})
|
||||
|
||||
# Send body
|
||||
await send({
|
||||
"type": "http.response.body",
|
||||
"body": response_body,
|
||||
"more_body": False,
|
||||
})
|
||||
|
||||
# Send trailers
|
||||
await send({
|
||||
"type": "http.response.trailers",
|
||||
"headers": [
|
||||
(b"content-md5", checksum.encode()),
|
||||
(b"x-processing-time", b"42ms"),
|
||||
],
|
||||
})
|
||||
else:
|
||||
# HTTP/1.1 fallback - include checksum in regular headers
|
||||
response = {
|
||||
"message": "Trailers not supported (HTTP/1.1)",
|
||||
"data": response_body.decode("utf-8", errors="replace"),
|
||||
"checksum_in_header": checksum,
|
||||
}
|
||||
body = json.dumps(response, indent=2).encode() + b"\n"
|
||||
await send_response(
|
||||
send, 200, body,
|
||||
content_type=b"application/json",
|
||||
extra_headers=[(b"x-checksum", checksum.encode())]
|
||||
)
|
||||
|
||||
|
||||
async def handle_combined(scope, receive, send):
|
||||
"""Show both priority and trailers in one response.
|
||||
|
||||
This demonstrates a realistic scenario like gRPC where
|
||||
priority affects scheduling and trailers carry status.
|
||||
"""
|
||||
extensions = scope.get("extensions", {})
|
||||
priority_info = extensions.get("http.response.priority")
|
||||
supports_trailers = "http.response.trailers" in extensions
|
||||
|
||||
# Build response showing all HTTP/2 features
|
||||
response = {
|
||||
"http_version": scope.get("http_version", "1.1"),
|
||||
"priority": None,
|
||||
"trailers_supported": supports_trailers,
|
||||
}
|
||||
|
||||
if priority_info:
|
||||
response["priority"] = {
|
||||
"weight": priority_info["weight"],
|
||||
"depends_on": priority_info["depends_on"],
|
||||
}
|
||||
|
||||
response_body = json.dumps(response, indent=2).encode() + b"\n"
|
||||
checksum = hashlib.md5(response_body).hexdigest()
|
||||
|
||||
if supports_trailers:
|
||||
# Full HTTP/2 response with trailers
|
||||
await send({
|
||||
"type": "http.response.start",
|
||||
"status": 200,
|
||||
"headers": [
|
||||
(b"content-type", b"application/json"),
|
||||
(b"trailer", b"content-md5, x-status"),
|
||||
],
|
||||
})
|
||||
|
||||
await send({
|
||||
"type": "http.response.body",
|
||||
"body": response_body,
|
||||
"more_body": False,
|
||||
})
|
||||
|
||||
await send({
|
||||
"type": "http.response.trailers",
|
||||
"headers": [
|
||||
(b"content-md5", checksum.encode()),
|
||||
(b"x-status", b"success"),
|
||||
],
|
||||
})
|
||||
else:
|
||||
await send_response(send, 200, response_body, content_type=b"application/json")
|
||||
|
||||
|
||||
async def send_response(send, status, body, content_type=b"text/plain", extra_headers=None):
|
||||
"""Send a simple HTTP response."""
|
||||
headers = [
|
||||
(b"content-type", content_type),
|
||||
(b"content-length", str(len(body)).encode()),
|
||||
]
|
||||
if extra_headers:
|
||||
headers.extend(extra_headers)
|
||||
|
||||
await send({
|
||||
"type": "http.response.start",
|
||||
"status": status,
|
||||
"headers": headers,
|
||||
})
|
||||
await send({
|
||||
"type": "http.response.body",
|
||||
"body": body,
|
||||
})
|
||||
|
||||
|
||||
async def read_body(receive):
|
||||
"""Read the full request body."""
|
||||
body = b""
|
||||
while True:
|
||||
message = await receive()
|
||||
body += message.get("body", b"")
|
||||
if not message.get("more_body", False):
|
||||
break
|
||||
return body
|
||||
2
examples/http2_features/requirements.txt
Normal file
2
examples/http2_features/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
# Requirements for testing HTTP/2 features
|
||||
httpx>=0.24.0
|
||||
291
examples/http2_features/test_http2.py
Normal file
291
examples/http2_features/test_http2.py
Normal file
@ -0,0 +1,291 @@
|
||||
#!/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())
|
||||
Loading…
x
Reference in New Issue
Block a user