Skip to content

Commit 5b50487

Browse files
authored
Merge pull request #3444 from benoitc/asgi-worker
Add native ASGI worker and uWSGI binary protocol support
2 parents ea98400 + 81b6534 commit 5b50487

36 files changed

Lines changed: 5193 additions & 6 deletions
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Docker Integration Tests
2+
3+
on:
4+
push:
5+
branches: [master]
6+
paths:
7+
- 'gunicorn/uwsgi/**'
8+
- 'tests/docker/uwsgi/**'
9+
- '.github/workflows/docker-integration.yml'
10+
pull_request:
11+
paths:
12+
- 'gunicorn/uwsgi/**'
13+
- 'tests/docker/uwsgi/**'
14+
- '.github/workflows/docker-integration.yml'
15+
16+
permissions:
17+
contents: read
18+
19+
env:
20+
FORCE_COLOR: 1
21+
22+
jobs:
23+
uwsgi-nginx:
24+
name: uWSGI Protocol with nginx
25+
runs-on: ubuntu-latest
26+
timeout-minutes: 15
27+
28+
steps:
29+
- uses: actions/checkout@v6
30+
31+
- name: Set up Python
32+
uses: actions/setup-python@v6
33+
with:
34+
python-version: "3.12"
35+
cache: pip
36+
cache-dependency-path: requirements_test.txt
37+
38+
- name: Install test dependencies
39+
run: |
40+
python -m pip install --upgrade pip
41+
python -m pip install pytest pytest-cov requests
42+
43+
- name: Run uWSGI integration tests
44+
run: |
45+
pytest tests/docker/uwsgi/ -v --tb=short

.github/workflows/freebsd.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
python${{ matrix.python-version }} -m venv venv
4141
. venv/bin/activate
4242
pip install --upgrade pip
43-
pip install pytest pytest-cov coverage
43+
pip install pytest pytest-cov pytest-asyncio coverage
4444
pip install -e .
4545
pytest --cov=gunicorn -v tests/ \
4646
--ignore=tests/workers/test_ggevent.py \

examples/asgi/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#
2+
# This file is part of gunicorn released under the MIT license.
3+
# See the NOTICE for more information.
4+
5+
"""
6+
ASGI example applications for gunicorn.
7+
"""

examples/asgi/basic_app.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#
2+
# This file is part of gunicorn released under the MIT license.
3+
# See the NOTICE for more information.
4+
5+
"""
6+
Basic ASGI application example.
7+
8+
Run with:
9+
gunicorn -k asgi examples.asgi.basic_app:app
10+
11+
Test with:
12+
curl http://127.0.0.1:8000/
13+
curl http://127.0.0.1:8000/hello
14+
curl -X POST http://127.0.0.1:8000/echo -d "test data"
15+
"""
16+
17+
18+
async def app(scope, receive, send):
19+
"""Simple ASGI application demonstrating basic functionality."""
20+
21+
if scope["type"] == "lifespan":
22+
await handle_lifespan(scope, receive, send)
23+
elif scope["type"] == "http":
24+
await handle_http(scope, receive, send)
25+
else:
26+
raise ValueError(f"Unknown scope type: {scope['type']}")
27+
28+
29+
async def handle_lifespan(scope, receive, send):
30+
"""Handle lifespan events (startup/shutdown)."""
31+
while True:
32+
message = await receive()
33+
if message["type"] == "lifespan.startup":
34+
print("ASGI application starting up...")
35+
await send({"type": "lifespan.startup.complete"})
36+
elif message["type"] == "lifespan.shutdown":
37+
print("ASGI application shutting down...")
38+
await send({"type": "lifespan.shutdown.complete"})
39+
return
40+
41+
42+
async def handle_http(scope, receive, send):
43+
"""Handle HTTP requests."""
44+
path = scope["path"]
45+
method = scope["method"]
46+
47+
if path == "/" and method == "GET":
48+
await send_response(send, 200, b"Welcome to gunicorn ASGI!\n")
49+
50+
elif path == "/hello" and method == "GET":
51+
name = get_query_param(scope, "name", "World")
52+
body = f"Hello, {name}!\n".encode()
53+
await send_response(send, 200, body)
54+
55+
elif path == "/echo" and method == "POST":
56+
body = await read_body(receive)
57+
await send_response(send, 200, body, content_type=b"application/octet-stream")
58+
59+
elif path == "/headers":
60+
headers_info = format_headers(scope["headers"])
61+
await send_response(send, 200, headers_info.encode())
62+
63+
elif path == "/info":
64+
info = format_request_info(scope)
65+
await send_response(send, 200, info.encode(), content_type=b"application/json")
66+
67+
else:
68+
await send_response(send, 404, b"Not Found\n")
69+
70+
71+
async def send_response(send, status, body, content_type=b"text/plain"):
72+
"""Send an HTTP response."""
73+
await send({
74+
"type": "http.response.start",
75+
"status": status,
76+
"headers": [
77+
(b"content-type", content_type),
78+
(b"content-length", str(len(body)).encode()),
79+
],
80+
})
81+
await send({
82+
"type": "http.response.body",
83+
"body": body,
84+
})
85+
86+
87+
async def read_body(receive):
88+
"""Read the full request body."""
89+
body = b""
90+
while True:
91+
message = await receive()
92+
body += message.get("body", b"")
93+
if not message.get("more_body", False):
94+
break
95+
return body
96+
97+
98+
def get_query_param(scope, name, default=None):
99+
"""Get a query parameter value."""
100+
query_string = scope.get("query_string", b"").decode()
101+
for param in query_string.split("&"):
102+
if "=" in param:
103+
key, value = param.split("=", 1)
104+
if key == name:
105+
return value
106+
return default
107+
108+
109+
def format_headers(headers):
110+
"""Format headers for display."""
111+
lines = ["Request Headers:"]
112+
for name, value in headers:
113+
lines.append(f" {name.decode()}: {value.decode()}")
114+
return "\n".join(lines) + "\n"
115+
116+
117+
def format_request_info(scope):
118+
"""Format request info as JSON."""
119+
import json
120+
info = {
121+
"method": scope["method"],
122+
"path": scope["path"],
123+
"query_string": scope.get("query_string", b"").decode(),
124+
"http_version": scope["http_version"],
125+
"scheme": scope["scheme"],
126+
"server": list(scope.get("server") or []),
127+
"client": list(scope.get("client") or []),
128+
"root_path": scope.get("root_path", ""),
129+
}
130+
return json.dumps(info, indent=2) + "\n"

0 commit comments

Comments
 (0)