mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-02 10:41: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}")
|
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
|
## See Also
|
||||||
|
|
||||||
- [Settings Reference](reference/settings.md#http2_max_concurrent_streams) - All HTTP/2 settings
|
- [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