Benoit Chesneau fa5e319f15 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
2026-01-27 13:15:36 +01:00

271 lines
8.5 KiB
Python

#
# 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