Add uWSGI protocol support to ASGI worker (#3467)

Add uWSGI protocol support to ASGI worker

- Implements AsyncUWSGIRequest class extending sync UWSGIRequest to reuse parsing logic with async I/O
- ASGI protocol handler selects between HTTP and uWSGI based on --protocol config option
- Allows gunicorn's ASGI worker to receive requests from nginx using uwsgi_pass directive
- Includes unit tests and Docker integration tests
This commit is contained in:
Benoit Chesneau 2026-01-25 14:45:07 +01:00 committed by GitHub
parent be6f3b97ab
commit 8663740907
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 890 additions and 8 deletions

View File

@ -33,6 +33,7 @@ The ASGI worker provides:
- **Lifespan protocol** for startup/shutdown hooks - **Lifespan protocol** for startup/shutdown hooks
- **Optional uvloop** for improved performance - **Optional uvloop** for improved performance
- **SSL/TLS** support - **SSL/TLS** support
- **uWSGI protocol** for nginx `uwsgi_pass` integration
## Configuration ## Configuration
@ -151,7 +152,7 @@ app = Starlette(routes=[
## Production Deployment ## Production Deployment
### With Nginx ### With Nginx (HTTP Proxy)
```nginx ```nginx
upstream gunicorn { upstream gunicorn {
@ -181,6 +182,36 @@ server {
} }
``` ```
### With Nginx (uWSGI Protocol)
For better performance, you can use nginx's native uWSGI protocol support:
```bash
gunicorn myapp:app --worker-class asgi --protocol uwsgi --bind 127.0.0.1:8000
```
```nginx
upstream gunicorn {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name example.com;
location / {
uwsgi_pass gunicorn;
include uwsgi_params;
}
}
```
!!! note
WebSocket connections are not supported when using the uWSGI protocol.
Use HTTP proxy for WebSocket endpoints.
See [uWSGI Protocol](uwsgi.md) for more details on uWSGI protocol configuration.
### Recommended Settings ### Recommended Settings
For production ASGI deployments: For production ASGI deployments:

View File

@ -4,6 +4,8 @@ Gunicorn supports the uWSGI binary protocol, allowing it to receive requests fro
nginx using the `uwsgi_pass` directive. This provides efficient communication nginx using the `uwsgi_pass` directive. This provides efficient communication
between nginx and Gunicorn without HTTP overhead. between nginx and Gunicorn without HTTP overhead.
Both **WSGI** and **ASGI** workers support the uWSGI protocol.
!!! note !!! note
This is the **uWSGI binary protocol**, not the uWSGI server. Gunicorn This is the **uWSGI binary protocol**, not the uWSGI server. Gunicorn
implements the protocol to receive requests from nginx, similar to how implements the protocol to receive requests from nginx, similar to how
@ -14,7 +16,11 @@ between nginx and Gunicorn without HTTP overhead.
Enable uWSGI protocol support: Enable uWSGI protocol support:
```bash ```bash
# WSGI application
gunicorn myapp:app --protocol uwsgi --bind 127.0.0.1:8000 gunicorn myapp:app --protocol uwsgi --bind 127.0.0.1:8000
# ASGI application
gunicorn myapp:app --worker-class asgi --protocol uwsgi --bind 127.0.0.1:8000
``` ```
Configure nginx to forward requests: Configure nginx to forward requests:

View File

@ -14,7 +14,9 @@ from datetime import datetime
from gunicorn.asgi.unreader import AsyncUnreader from gunicorn.asgi.unreader import AsyncUnreader
from gunicorn.asgi.message import AsyncRequest from gunicorn.asgi.message import AsyncRequest
from gunicorn.asgi.uwsgi import AsyncUWSGIRequest
from gunicorn.http.errors import NoMoreData from gunicorn.http.errors import NoMoreData
from gunicorn.uwsgi.errors import UWSGIParseException
class ASGIResponseInfo: class ASGIResponseInfo:
@ -92,19 +94,31 @@ class ASGIProtocol(asyncio.Protocol):
self.req_count += 1 self.req_count += 1
try: try:
# Parse HTTP request # Parse request based on protocol
request = await AsyncRequest.parse( protocol = getattr(self.cfg, 'protocol', 'http')
self.cfg, if protocol == 'uwsgi':
unreader, request = await AsyncUWSGIRequest.parse(
peername, self.cfg,
self.req_count unreader,
) peername,
self.req_count
)
else:
request = await AsyncRequest.parse(
self.cfg,
unreader,
peername,
self.req_count
)
except StopIteration: except StopIteration:
# No more data, close connection # No more data, close connection
break break
except NoMoreData: except NoMoreData:
# Client disconnected # Client disconnected
break break
except UWSGIParseException as e:
self.log.debug("uWSGI parse error: %s", e)
break
# Check for WebSocket upgrade # Check for WebSocket upgrade
if self._is_websocket_upgrade(request): if self._is_websocket_upgrade(request):

172
gunicorn/asgi/uwsgi.py Normal file
View File

@ -0,0 +1,172 @@
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""Async uWSGI protocol parser for ASGI workers.
Reuses the parsing logic from gunicorn/uwsgi/message.py, only async I/O differs.
"""
from gunicorn.uwsgi.message import UWSGIRequest
from gunicorn.uwsgi.errors import (
InvalidUWSGIHeader,
UnsupportedModifier,
)
class AsyncUWSGIRequest(UWSGIRequest):
"""Async version of UWSGIRequest.
Reuses all parsing logic from the sync version, only async I/O differs.
The following methods are reused from the parent class:
- _parse_vars() - pure parsing, no I/O
- _extract_request_info() - pure transformation
- _check_allowed_ip() - no I/O
- should_close() - simple logic
"""
# pylint: disable=super-init-not-called
def __init__(self, cfg, unreader, peer_addr, req_number=1):
# Don't call super().__init__ - it does sync parsing
# Just initialize attributes
self.cfg = cfg
self.unreader = unreader
self.peer_addr = peer_addr
self.remote_addr = peer_addr
self.req_number = req_number
# Initialize all attributes (same as sync version)
self.method = None
self.uri = None
self.path = None
self.query = None
self.fragment = ""
self.version = (1, 1)
self.headers = []
self.trailers = []
self.body = None
self.scheme = "https" if cfg.is_ssl else "http"
self.must_close = False
self.uwsgi_vars = {}
self.modifier1 = 0
self.modifier2 = 0
self.proxy_protocol_info = None
# Body state
self.content_length = 0
self.chunked = False
self._body_remaining = 0
# Async factory method - intentionally differs from sync parent:
# - async instead of sync (invalid-overridden-method)
# - different signature for async I/O (arguments-differ)
# pylint: disable=arguments-differ,invalid-overridden-method
@classmethod
async def parse(cls, cfg, unreader, peer_addr, req_number=1):
"""Parse a uWSGI request asynchronously.
Args:
cfg: gunicorn config object
unreader: AsyncUnreader instance
peer_addr: client address tuple
req_number: request number on this connection (for keepalive)
Returns:
AsyncUWSGIRequest: Parsed request object
Raises:
InvalidUWSGIHeader: If the uWSGI header is malformed
UnsupportedModifier: If modifier1 is not 0
ForbiddenUWSGIRequest: If source IP is not allowed
"""
req = cls(cfg, unreader, peer_addr, req_number)
req._check_allowed_ip() # Reuse from parent
await req._async_parse()
return req
async def _async_parse(self):
"""Async version of parse() - reads data then uses sync parsing."""
# Read 4-byte header
header = await self._async_read_exact(4)
if len(header) < 4:
raise InvalidUWSGIHeader("incomplete header")
self.modifier1 = header[0]
datasize = int.from_bytes(header[1:3], 'little')
self.modifier2 = header[3]
if self.modifier1 != 0:
raise UnsupportedModifier(self.modifier1)
# Read vars block
if datasize > 0:
vars_data = await self._async_read_exact(datasize)
if len(vars_data) < datasize:
raise InvalidUWSGIHeader("incomplete vars block")
self._parse_vars(vars_data) # Reuse sync method
self._extract_request_info() # Reuse sync method
self._set_body_reader()
async def _async_read_exact(self, size):
"""Read exactly size bytes asynchronously."""
buf = bytearray()
while len(buf) < size:
chunk = await self.unreader.read(size - len(buf))
if not chunk:
break
buf.extend(chunk)
return bytes(buf)
def _set_body_reader(self):
"""Set up body state for async reading."""
content_length = 0
if 'CONTENT_LENGTH' in self.uwsgi_vars:
try:
content_length = max(int(self.uwsgi_vars['CONTENT_LENGTH']), 0)
except ValueError:
content_length = 0
self.content_length = content_length
self._body_remaining = content_length
async def read_body(self, size=8192):
"""Read body chunk asynchronously.
Args:
size: Maximum bytes to read
Returns:
bytes: Body data, empty bytes when body is exhausted
"""
if self._body_remaining <= 0:
return b""
to_read = min(size, self._body_remaining)
data = await self.unreader.read(to_read)
if data:
self._body_remaining -= len(data)
return data
async def drain_body(self):
"""Drain unread body data.
Should be called before reusing connection for keepalive.
"""
while self._body_remaining > 0:
data = await self.read_body(8192)
if not data:
break
def get_header(self, name):
"""Get header by name (case-insensitive).
Args:
name: Header name to look up
Returns:
Header value if found, None otherwise
"""
name = name.upper()
for h, v in self.headers:
if h == name:
return v
return None

View File

@ -0,0 +1,18 @@
FROM python:3.11-slim
WORKDIR /build
# Copy gunicorn source
COPY . /build/
# Install gunicorn from source
RUN pip install --no-cache-dir -e .
# Copy test app
WORKDIR /app
COPY tests/docker/test_asgi_uwsgi/app.py /app/
# Expose uWSGI port
EXPOSE 8000
CMD ["gunicorn", "--worker-class", "asgi", "--protocol", "uwsgi", "--bind", "0.0.0.0:8000", "app:app"]

View File

@ -0,0 +1,45 @@
"""Simple ASGI test application for uWSGI protocol testing."""
async def app(scope, receive, send):
"""Simple ASGI application that echoes request info."""
if scope["type"] == "lifespan":
while True:
message = await receive()
if message["type"] == "lifespan.startup":
await send({"type": "lifespan.startup.complete"})
elif message["type"] == "lifespan.shutdown":
await send({"type": "lifespan.shutdown.complete"})
return
if scope["type"] != "http":
return
# Read body
body = b""
while True:
message = await receive()
body += message.get("body", b"")
if not message.get("more_body", False):
break
# Build response
method = scope["method"]
path = scope["path"]
query = scope.get("query_string", b"").decode("utf-8")
response_body = f"Method: {method}\nPath: {path}\nQuery: {query}\nBody: {body.decode('utf-8')}\n"
response_bytes = response_body.encode("utf-8")
await send({
"type": "http.response.start",
"status": 200,
"headers": [
[b"content-type", b"text/plain"],
[b"content-length", str(len(response_bytes)).encode()],
],
})
await send({
"type": "http.response.body",
"body": response_bytes,
})

View File

@ -0,0 +1,24 @@
services:
gunicorn:
build:
context: ../../..
dockerfile: tests/docker/test_asgi_uwsgi/Dockerfile
command: >
gunicorn
--worker-class asgi
--protocol uwsgi
--uwsgi-allow-from '*'
--bind 0.0.0.0:8000
--workers 1
--log-level debug
app:app
working_dir: /app
nginx:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- gunicorn

View File

@ -0,0 +1,14 @@
server {
listen 80;
server_name localhost;
location / {
uwsgi_pass gunicorn:8000;
include uwsgi_params;
}
location /health {
return 200 "OK";
add_header Content-Type text/plain;
}
}

View File

@ -0,0 +1,86 @@
#!/bin/bash
# Integration test for ASGI uWSGI protocol support
#
# This script tests that gunicorn's ASGI worker correctly handles
# the uWSGI protocol when nginx forwards requests using uwsgi_pass.
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Use IPv4 explicitly to avoid Docker IPv6 issues
BASE_URL="http://127.0.0.1:8080"
cleanup() {
echo "Cleaning up..."
docker compose down -v 2>/dev/null || true
}
trap cleanup EXIT
echo "=== Building and starting containers ==="
docker compose up -d --build
echo "=== Waiting for services to be ready ==="
sleep 5
echo "=== Running tests ==="
# Test 1: Simple GET request
echo "Test 1: Simple GET request"
RESPONSE=$(curl -s "$BASE_URL/")
if echo "$RESPONSE" | grep -q "Method: GET"; then
echo " PASS: GET request works"
else
echo " FAIL: GET request failed"
echo " Response: $RESPONSE"
exit 1
fi
# Test 2: GET with query string
echo "Test 2: GET with query string"
RESPONSE=$(curl -s "$BASE_URL/search?q=test&page=1")
if echo "$RESPONSE" | grep -q "Query: q=test&page=1"; then
echo " PASS: Query string works"
else
echo " FAIL: Query string failed"
echo " Response: $RESPONSE"
exit 1
fi
# Test 3: POST with body
echo "Test 3: POST with body"
RESPONSE=$(curl -s -X POST -d "hello=world" "$BASE_URL/submit")
if echo "$RESPONSE" | grep -q "Method: POST" && echo "$RESPONSE" | grep -q "Body: hello=world"; then
echo " PASS: POST with body works"
else
echo " FAIL: POST with body failed"
echo " Response: $RESPONSE"
exit 1
fi
# Test 4: Path handling
echo "Test 4: Path handling"
RESPONSE=$(curl -s "$BASE_URL/api/v1/users")
if echo "$RESPONSE" | grep -q "Path: /api/v1/users"; then
echo " PASS: Path handling works"
else
echo " FAIL: Path handling failed"
echo " Response: $RESPONSE"
exit 1
fi
# Test 5: Multiple requests (keepalive)
echo "Test 5: Multiple requests (keepalive)"
for i in 1 2 3; do
RESPONSE=$(curl -s "$BASE_URL/request/$i")
if ! echo "$RESPONSE" | grep -q "Path: /request/$i"; then
echo " FAIL: Request $i failed"
exit 1
fi
done
echo " PASS: Multiple requests work"
echo ""
echo "=== All tests passed! ==="

472
tests/test_asgi_uwsgi.py Normal file
View File

@ -0,0 +1,472 @@
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""
Tests for ASGI uWSGI protocol parser.
"""
import pytest
from gunicorn.asgi.unreader import AsyncUnreader
from gunicorn.asgi.uwsgi import AsyncUWSGIRequest
from gunicorn.uwsgi.errors import (
InvalidUWSGIHeader,
UnsupportedModifier,
ForbiddenUWSGIRequest,
)
class MockStreamReader:
"""Mock asyncio.StreamReader for testing."""
def __init__(self, data):
self.data = data
self.pos = 0
async def read(self, size=-1):
if self.pos >= len(self.data):
return b""
if size < 0:
result = self.data[self.pos:]
self.pos = len(self.data)
else:
result = self.data[self.pos:self.pos + size]
self.pos += size
return result
class MockConfig:
"""Mock gunicorn config for testing."""
def __init__(self):
self.is_ssl = False
self.uwsgi_allow_ips = ['*'] # Allow all for most tests
def build_uwsgi_packet(vars_dict, modifier1=0, modifier2=0):
"""Build a uWSGI packet from a dictionary of variables.
Args:
vars_dict: Dictionary of uWSGI variables
modifier1: uWSGI modifier1 (default 0 for WSGI)
modifier2: uWSGI modifier2 (default 0)
Returns:
bytes: Complete uWSGI packet
"""
vars_data = b""
for key, value in vars_dict.items():
key_bytes = key.encode('latin-1')
value_bytes = value.encode('latin-1')
vars_data += len(key_bytes).to_bytes(2, 'little')
vars_data += key_bytes
vars_data += len(value_bytes).to_bytes(2, 'little')
vars_data += value_bytes
# Build header: modifier1 (1 byte) + datasize (2 bytes LE) + modifier2 (1 byte)
header = bytes([modifier1])
header += len(vars_data).to_bytes(2, 'little')
header += bytes([modifier2])
return header + vars_data
# Basic parsing tests
@pytest.mark.asyncio
async def test_parse_simple_get():
"""Test parsing a simple GET request."""
vars_dict = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/test',
'QUERY_STRING': '',
'HTTP_HOST': 'localhost',
}
packet = build_uwsgi_packet(vars_dict)
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
assert request.method == "GET"
assert request.path == "/test"
assert request.query == ""
assert request.uri == "/test"
assert request.version == (1, 1)
@pytest.mark.asyncio
async def test_parse_get_with_query():
"""Test parsing GET request with query string."""
vars_dict = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/search',
'QUERY_STRING': 'q=test&page=1',
}
packet = build_uwsgi_packet(vars_dict)
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
assert request.method == "GET"
assert request.path == "/search"
assert request.query == "q=test&page=1"
assert request.uri == "/search?q=test&page=1"
@pytest.mark.asyncio
async def test_parse_post_with_content_length():
"""Test parsing POST request with content length."""
body = b"hello=world"
vars_dict = {
'REQUEST_METHOD': 'POST',
'PATH_INFO': '/submit',
'CONTENT_LENGTH': str(len(body)),
'CONTENT_TYPE': 'application/x-www-form-urlencoded',
}
packet = build_uwsgi_packet(vars_dict) + body
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
assert request.method == "POST"
assert request.path == "/submit"
assert request.content_length == len(body)
# Read body
read_body = await request.read_body(100)
assert read_body == body
@pytest.mark.asyncio
async def test_parse_headers():
"""Test that HTTP headers are correctly extracted."""
vars_dict = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/',
'HTTP_HOST': 'example.com',
'HTTP_ACCEPT': 'text/html',
'HTTP_X_CUSTOM_HEADER': 'custom-value',
'CONTENT_TYPE': 'text/plain',
'CONTENT_LENGTH': '0',
}
packet = build_uwsgi_packet(vars_dict)
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
# Check headers were extracted correctly
assert request.get_header('HOST') == 'example.com'
assert request.get_header('ACCEPT') == 'text/html'
assert request.get_header('X-CUSTOM-HEADER') == 'custom-value'
assert request.get_header('CONTENT-TYPE') == 'text/plain'
assert request.get_header('CONTENT-LENGTH') == '0'
@pytest.mark.asyncio
async def test_parse_https_scheme():
"""Test HTTPS scheme detection."""
vars_dict = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/',
'HTTPS': 'on',
}
packet = build_uwsgi_packet(vars_dict)
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
assert request.scheme == 'https'
@pytest.mark.asyncio
async def test_parse_wsgi_url_scheme():
"""Test wsgi.url_scheme variable."""
vars_dict = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/',
'wsgi.url_scheme': 'https',
}
packet = build_uwsgi_packet(vars_dict)
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
assert request.scheme == 'https'
# Body reading tests
@pytest.mark.asyncio
async def test_read_body_chunks():
"""Test reading body in chunks."""
body = b"a" * 100
vars_dict = {
'REQUEST_METHOD': 'POST',
'PATH_INFO': '/',
'CONTENT_LENGTH': str(len(body)),
}
packet = build_uwsgi_packet(vars_dict) + body
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
# Read in chunks
chunks = []
while True:
chunk = await request.read_body(30)
if not chunk:
break
chunks.append(chunk)
assert b"".join(chunks) == body
@pytest.mark.asyncio
async def test_drain_body():
"""Test draining unread body."""
body = b"x" * 50
vars_dict = {
'REQUEST_METHOD': 'POST',
'PATH_INFO': '/',
'CONTENT_LENGTH': str(len(body)),
}
packet = build_uwsgi_packet(vars_dict) + body
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
# Drain without reading
await request.drain_body()
# Further reads should return empty
chunk = await request.read_body()
assert chunk == b""
@pytest.mark.asyncio
async def test_no_body():
"""Test request with no body."""
vars_dict = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/',
}
packet = build_uwsgi_packet(vars_dict)
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
assert request.content_length == 0
chunk = await request.read_body()
assert chunk == b""
# Connection handling tests
@pytest.mark.asyncio
async def test_should_close_default():
"""Test default keepalive behavior."""
vars_dict = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/',
}
packet = build_uwsgi_packet(vars_dict)
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
# Default should be keep-alive (HTTP/1.1 behavior)
assert request.should_close() is False
@pytest.mark.asyncio
async def test_should_close_connection_close():
"""Test connection close header."""
vars_dict = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/',
'HTTP_CONNECTION': 'close',
}
packet = build_uwsgi_packet(vars_dict)
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
assert request.should_close() is True
@pytest.mark.asyncio
async def test_should_close_keepalive():
"""Test connection keep-alive header."""
vars_dict = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/',
'HTTP_CONNECTION': 'keep-alive',
}
packet = build_uwsgi_packet(vars_dict)
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
assert request.should_close() is False
# Error handling tests
@pytest.mark.asyncio
async def test_incomplete_header():
"""Test incomplete header raises error."""
# Only 2 bytes instead of 4
data = b"\x00\x00"
reader = MockStreamReader(data)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
with pytest.raises(InvalidUWSGIHeader):
await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
@pytest.mark.asyncio
async def test_unsupported_modifier():
"""Test unsupported modifier1 raises error."""
# modifier1 = 1 (not WSGI)
header = bytes([1, 0, 0, 0]) # modifier1=1, datasize=0, modifier2=0
reader = MockStreamReader(header)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
with pytest.raises(UnsupportedModifier):
await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
@pytest.mark.asyncio
async def test_incomplete_vars_block():
"""Test incomplete vars block raises error."""
# Header says 100 bytes of vars, but only 10 provided
header = bytes([0]) # modifier1=0
header += (100).to_bytes(2, 'little') # datasize=100
header += bytes([0]) # modifier2=0
header += b"x" * 10 # Only 10 bytes
reader = MockStreamReader(header)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
with pytest.raises(InvalidUWSGIHeader):
await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
@pytest.mark.asyncio
async def test_forbidden_ip():
"""Test forbidden IP raises error."""
vars_dict = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/',
}
packet = build_uwsgi_packet(vars_dict)
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
cfg.uwsgi_allow_ips = ['10.0.0.1'] # Only allow 10.0.0.1
with pytest.raises(ForbiddenUWSGIRequest):
await AsyncUWSGIRequest.parse(cfg, unreader, ("192.168.1.1", 8000))
@pytest.mark.asyncio
async def test_allowed_ip():
"""Test allowed IP succeeds."""
vars_dict = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/',
}
packet = build_uwsgi_packet(vars_dict)
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
cfg.uwsgi_allow_ips = ['192.168.1.1']
# Should not raise
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("192.168.1.1", 8000))
assert request.method == "GET"
@pytest.mark.asyncio
async def test_unix_socket_allowed():
"""Test UNIX socket connections are always allowed."""
vars_dict = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/',
}
packet = build_uwsgi_packet(vars_dict)
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
cfg.uwsgi_allow_ips = ['10.0.0.1'] # Restrictive IP list
# UNIX socket peer_addr is not a tuple
request = await AsyncUWSGIRequest.parse(cfg, unreader, "/tmp/gunicorn.sock")
assert request.method == "GET"
# Empty vars block test
@pytest.mark.asyncio
async def test_empty_vars_block():
"""Test request with empty vars block uses defaults."""
# Header with datasize=0
header = bytes([0, 0, 0, 0])
reader = MockStreamReader(header)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
# Should use defaults
assert request.method == "GET"
assert request.path == "/"
assert request.query == ""
# SSL config test
@pytest.mark.asyncio
async def test_ssl_config_scheme():
"""Test SSL config sets https scheme."""
vars_dict = {
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/',
}
packet = build_uwsgi_packet(vars_dict)
reader = MockStreamReader(packet)
unreader = AsyncUnreader(reader)
cfg = MockConfig()
cfg.is_ssl = True
request = await AsyncUWSGIRequest.parse(cfg, unreader, ("127.0.0.1", 8000))
assert request.scheme == 'https'